feat(security): rate-limit public share endpoint + security headers#52
Merged
Conversation
Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate limiting. Add flask-limiter (in-memory, single-process MVP) with: - 60 req/min/IP on view_share (Vault §2.S1) - Global default 600 req/min on all other routes (non-blocking baseline) - Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options headers on every public share response (Vault §2.S2) The Limiter singleton lives in rate_limit.py to break the circular-import chain between app.py (which imports route blueprints) and the blueprints that need @limiter.limit() decorators. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reviewer's GuideAdds Flask-Limiter-based rate limiting and security headers to the public share view endpoint, introduces a shared limiter singleton, and wires the new dependency into the project configuration. Sequence diagram for rate-limited public share view endpointsequenceDiagram
actor Client
participant FlaskApp
participant Limiter
participant SharesBlueprint
participant AfterThisRequestHandler
Client->>FlaskApp: GET /api/shares/<token>/view
FlaskApp->>Limiter: check_rate_limit(remote_address, 60 per minute)
alt over_limit
Limiter-->>FlaskApp: limit_exceeded
FlaskApp-->>Client: 429 Too Many Requests + Retry-After
else within_limit
Limiter-->>FlaskApp: allowed
FlaskApp->>SharesBlueprint: call view_share(token)
SharesBlueprint->>AfterThisRequestHandler: register _add_security_headers
SharesBlueprint-->>FlaskApp: Response(file_content)
FlaskApp->>AfterThisRequestHandler: apply _add_security_headers(Response)
AfterThisRequestHandler-->>FlaskApp: Response with security headers
FlaskApp-->>Client: 200 OK + file + security headers
end
Class diagram for limiter singleton and shares endpoint integrationclassDiagram
class Limiter {
+list default_limits
+string storage_uri
+Limiter callable key_func
+init_app(app)
+limit(limit_string)
}
class rate_limit {
+Limiter limiter
}
class SharesBlueprint {
+view_share(token)
}
rate_limit --> Limiter : creates_instance_of
SharesBlueprint --> rate_limit : imports_limiter
SharesBlueprint ..> Limiter : uses_limit_decorator_on_view_share
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The
@after_this_requesthandler is defined after several earlyreturnpaths inview_share, so 404/410 and other error responses from this endpoint will not receive the new security headers; consider registering the handler immediately after entering the view to cover all responses. - The import
from rate_limit import limitermay be fragile depending on how the backend package is laid out; using a relative import such asfrom .rate_limit import limitercan avoid issues when the app is imported as a package.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `@after_this_request` handler is defined after several early `return` paths in `view_share`, so 404/410 and other error responses from this endpoint will not receive the new security headers; consider registering the handler immediately after entering the view to cover all responses.
- The import `from rate_limit import limiter` may be fragile depending on how the backend package is laid out; using a relative import such as `from .rate_limit import limiter` can avoid issues when the app is imported as a package.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
7 tasks
DavidsonGomes
added a commit
that referenced
this pull request
Apr 25, 2026
…ortals (#53) * feat(security): rate-limit public share endpoint + security headers Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate limiting. Add flask-limiter (in-memory, single-process MVP) with: - 60 req/min/IP on view_share (Vault §2.S1) - Global default 600 req/min on all other routes (non-blocking baseline) - Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options headers on every public share response (Vault §2.S2) The Limiter singleton lives in rate_limit.py to break the circular-import chain between app.py (which imports route blueprints) and the blueprints that need @limiter.limit() decorators. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): B2.0 public_pages capability — read-only token-bound portals Add the public_pages plugin capability (B2.0 scope, read-only): plugin_schema.py: - Add Capability.public_pages and Capability.safe_uninstall enum values - Add PluginPublicPageTokenSource + PluginPublicPage Pydantic models (bundle must be under ui/public/, revoked_when disallowed in v1 to prevent SQL injection) - Extend ReadonlyQuery with public_via + bind_token_param fields - Add 4 PluginManifest model validators: capability required, table slug-prefix, unique ids/route_prefixes, readonly_data references valid page routes/plugin_public_pages.py (new): - GET /p/<slug>/<route_prefix>/<token> — serve portal bundle (60 req/min/IP) - GET /p/<slug>/<route_prefix>/<token>/data — serve token-bound readonly query (120/min) - GET /p/<slug>/public-assets/<path> — serve ui/public/ static assets - Token validation via parametric SQL (identifiers validated at install by schema) - Module-level _PLUGIN_PUBLIC_PREFIXES cache for install/uninstall lifecycle - Vault §B2.S2: Referrer-Policy, Cache-Control no-store, HSTS, X-Content-Type-Options on all responses - CSP: default-src 'self' on portal bundles - Rate limiting via rate_limit.limiter (imported from PR #52) app.py: - Import and register plugin_public_pages_bp - /p/... paths already bypass auth_middleware (non-/api/ paths are passthrough) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
3 tasks
DavidsonGomes
added a commit
that referenced
this pull request
Apr 26, 2026
) Bumps version to 0.33.0 so the plugin nutri (which requires this version) can install. Bundles the five plugin-contract PRs (#52→#56) merged today into a single release. Plus a UX fix on the install wizard so 409s say why they conflicted. The fix - lib/api.ts buildError now falls back to data.conflicts[0] when the standard error/message fields are absent. The plugin preview endpoint returns {conflicts: string[], manifest, ...} on 409 — without this fix the wizard showed only "409 CONFLICT" with the actual reason hidden. - PluginInstallModal: conflicts type was Record<string, unknown>, backend always returned string[]; the JSON.keys() coercion produced index strings. Now typed as string[] and rendered as a list. Tested - Frontend tsc --noEmit clean - Plugin nutri 200 pytest still pass after the 11 `# nosec B603` markers added to subprocess.run calls (false positives from regex security scan — all calls use list args, no shell=True) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jbmendonca
added a commit
to jbmendonca/evo-nexus
that referenced
this pull request
Apr 27, 2026
v0.33.0 — plugin contract bundle (PRs evolution-foundation#52→evolution-foundation#56) + install error surfacing
jbmendonca
pushed a commit
to jbmendonca/evo-nexus
that referenced
this pull request
May 15, 2026
…volution-foundation#52) Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate limiting. Add flask-limiter (in-memory, single-process MVP) with: - 60 req/min/IP on view_share (Vault §2.S1) - Global default 600 req/min on all other routes (non-blocking baseline) - Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options headers on every public share response (Vault §2.S2) The Limiter singleton lives in rate_limit.py to break the circular-import chain between app.py (which imports route blueprints) and the blueprints that need @limiter.limit() decorators. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
jbmendonca
pushed a commit
to jbmendonca/evo-nexus
that referenced
this pull request
May 15, 2026
…ortals (evolution-foundation#53) * feat(security): rate-limit public share endpoint + security headers Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate limiting. Add flask-limiter (in-memory, single-process MVP) with: - 60 req/min/IP on view_share (Vault §2.S1) - Global default 600 req/min on all other routes (non-blocking baseline) - Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options headers on every public share response (Vault §2.S2) The Limiter singleton lives in rate_limit.py to break the circular-import chain between app.py (which imports route blueprints) and the blueprints that need @limiter.limit() decorators. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): B2.0 public_pages capability — read-only token-bound portals Add the public_pages plugin capability (B2.0 scope, read-only): plugin_schema.py: - Add Capability.public_pages and Capability.safe_uninstall enum values - Add PluginPublicPageTokenSource + PluginPublicPage Pydantic models (bundle must be under ui/public/, revoked_when disallowed in v1 to prevent SQL injection) - Extend ReadonlyQuery with public_via + bind_token_param fields - Add 4 PluginManifest model validators: capability required, table slug-prefix, unique ids/route_prefixes, readonly_data references valid page routes/plugin_public_pages.py (new): - GET /p/<slug>/<route_prefix>/<token> — serve portal bundle (60 req/min/IP) - GET /p/<slug>/<route_prefix>/<token>/data — serve token-bound readonly query (120/min) - GET /p/<slug>/public-assets/<path> — serve ui/public/ static assets - Token validation via parametric SQL (identifiers validated at install by schema) - Module-level _PLUGIN_PUBLIC_PREFIXES cache for install/uninstall lifecycle - Vault §B2.S2: Referrer-Policy, Cache-Control no-store, HSTS, X-Content-Type-Options on all responses - CSP: default-src 'self' on portal bundles - Rate limiting via rate_limit.limiter (imported from PR evolution-foundation#52) app.py: - Import and register plugin_public_pages_bp - /p/... paths already bypass auth_middleware (non-/api/ paths are passthrough) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
jbmendonca
pushed a commit
to jbmendonca/evo-nexus
that referenced
this pull request
May 15, 2026
…volution-foundation#57) Bumps version to 0.33.0 so the plugin nutri (which requires this version) can install. Bundles the five plugin-contract PRs (evolution-foundation#52→evolution-foundation#56) merged today into a single release. Plus a UX fix on the install wizard so 409s say why they conflicted. The fix - lib/api.ts buildError now falls back to data.conflicts[0] when the standard error/message fields are absent. The plugin preview endpoint returns {conflicts: string[], manifest, ...} on 409 — without this fix the wizard showed only "409 CONFLICT" with the actual reason hidden. - PluginInstallModal: conflicts type was Record<string, unknown>, backend always returned string[]; the JSON.keys() coercion produced index strings. Now typed as string[] and rendered as a list. Tested - Frontend tsc --noEmit clean - Plugin nutri 200 pytest still pass after the 11 `# nosec B603` markers added to subprocess.run calls (false positives from regex security scan — all calls use list args, no shell=True) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
flask-limiter(in-memory, single-process) —60 req/min/IPon/api/shares/<token>/viewReferrer-Policy: no-referrer,Cache-Control: no-store,Pragma: no-cache,HSTS,X-Content-Type-Optionson every public share responseflask-limiter>=3.5topyproject.tomldependenciesdashboard/backend/rate_limit.pysingleton (breaks circular-import betweenapp.py→ blueprints)@limiter.limit()Test plan
test_cache_hit_does_not_call_compute_againfailure unrelated to this PR)/api/shares/<token>/view→ HTTP 429 +Retry-Afterheader🤖 Generated with Claude Code
Summary by Sourcery
Introduce request rate limiting and stricter security headers for public share endpoints.
New Features:
Enhancements: