Skip to content

feat(api): pre-release security hardening bundle - #620

Merged
andyne13 merged 3 commits into
refactor/hexagonalfrom
fix/pre-release-hardening
Jul 3, 2026
Merged

feat(api): pre-release security hardening bundle#620
andyne13 merged 3 commits into
refactor/hexagonalfrom
fix/pre-release-hardening

Conversation

@andyne13

@andyne13 andyne13 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Three small, independent pre-release hardening changes on the API surface.

Changes

Security response headers — new SecurityHeadersMiddleware sets X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, and Referrer-Policy: strict-origin-when-cross-origin on every response, plus Strict-Transport-Security only when the request is HTTPS (so local http development and http health probes are unaffected). setdefault preserves a stricter header a route sets for itself. A full CSP is intentionally omitted — the mounted admin UI / Chainlit rely on inline scripts/styles and would need per-UI tuning.

Reject a wildcard CORS origin with credentialssanitize_cors_origins drops any * from the allowlist when allow_credentials=True, so a misconfigured CORS_EXTRA_ORIGINS=* can't cause Starlette to reflect the request Origin with credentials.

Rate limiter parses only when enabledRATE_LIMIT_* values are now parsed only when the limiter is enabled, so a malformed value no longer crashes boot when rate limiting is turned off.

Not included (by design)

Tests

  • test_security_headers.py, test_cors_config.py, and a rate-limit test asserting a malformed config with limiting disabled doesn't crash.
  • Full unit suite green; ruff + layer-import checks pass.

Note: the main.py CORS block is also touched by #616 (the INDEXERUIADMIN_UI rewire); the two compose — sanitize_cors_origins applies to whichever origin list that PR produces — so expect a trivial merge.

Summary by CodeRabbit

  • New Features

    • Added baseline security headers to API responses, including CORS preflight and unhandled error responses.
    • Improved CORS handling by sanitizing wildcard origins when credentialed requests are enabled.
    • Enabled the same security headers for the standalone Chainlit server.
  • Bug Fixes

    • Rate limiting now ignores malformed limit configuration when rate limiting is disabled.
    • Default security headers no longer override stricter route-defined header values.
  • Tests

    • Added/expanded unit tests covering CORS sanitization, security header behavior (including error paths), and disabled rate-limiting config handling.

- Add SecurityHeadersMiddleware (X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, and HSTS over HTTPS) applied to every response.
- Reject a wildcard CORS origin when credentials are enabled
  (sanitize_cors_origins), so a misconfigured CORS_EXTRA_ORIGINS=* cannot
  reflect credentialed any-origin access.
- Parse RATE_LIMIT_* only when rate limiting is enabled, so a malformed value
  no longer crashes boot when the feature is off.

Adds unit tests for each; full unit suite green.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46157665-f98e-48af-ae10-1d9819944c7a

📥 Commits

Reviewing files that changed from the base of the PR and between af6593c and 14d1b67.

📒 Files selected for processing (1)
  • openrag/chainlit_api.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openrag/chainlit_api.py

📝 Walkthrough

Walkthrough

Adds CORS origin sanitization, security headers middleware and wiring, and a rate-limit startup guard so disabled rate limiting skips parsing malformed config.

Changes

API Security Hardening

Layer / File(s) Summary
CORS wildcard sanitization
openrag/api/cors_config.py, openrag/api/main.py, tests/unit/api/test_cors_config.py
Adds sanitize_cors_origins() to remove "*" when credentials are enabled, wires it into CORS setup, and adds tests for wildcard handling.
SecurityHeadersMiddleware implementation
openrag/api/middleware/security_headers.py, openrag/api/middleware/__init__.py
Adds baseline response security headers, HTTPS detection, conditional HSTS, and middleware package re-exports.
Security headers wiring and tests
openrag/api/main.py, openrag/chainlit_api.py, openrag/api/error_handlers.py, tests/unit/api/middleware/test_security_headers.py
Registers the middleware in both FastAPI apps, applies headers to unhandled 500 responses, and expands tests for baseline headers, HSTS, preservation, and error paths.
Rate-limit config parsing fix
openrag/api/middleware/rate_limit.py, tests/unit/api/middleware/test_rate_limit.py
Moves RATE_LIMIT_* parsing behind the enabled check and adds coverage for disabled malformed config.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main theme: API security hardening before release.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pre-release-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93c6e166ae

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread openrag/api/main.py Outdated
app.add_middleware(InstrumentationMiddleware)
# Registered last among the app stack so it wraps outermost and stamps the
# baseline security headers on every response that flows out.
app.add_middleware(SecurityHeadersMiddleware)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Place security headers outside CORS

When a browser sends a CORS preflight, the CORSMiddleware added later at line 305 is the outermost middleware and short-circuits OPTIONS requests before they reach this inner SecurityHeadersMiddleware (the surrounding comment already notes CORS preflights bypass the inner stack). In that scenario the new baseline headers, including HSTS for HTTPS preflights, are omitted even though the middleware is intended to cover every response; register the security middleware after CORS or otherwise wrap CORS with it.

Useful? React with 👍 / 👎.

Comment thread openrag/api/main.py Outdated
app.add_middleware(InstrumentationMiddleware)
# Registered last among the app stack so it wraps outermost and stamps the
# baseline security headers on every response that flows out.
app.add_middleware(SecurityHeadersMiddleware)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register security headers on standalone Chainlit

When ENABLE_RAY_SERVE=true and Chainlit is enabled, api.main imports chainlit_api.app and starts it in a separate uvicorn process on CHAINLIT_PORT, while openrag/chainlit_api.py defines its own FastAPI app serving /chainlit, /static, and /assets. This middleware is only installed on the main API app here, so that public Ray Serve Chainlit origin still lacks the new nosniff, frame, referrer, and HSTS hardening; add the same middleware to the standalone Chainlit app as well.

Useful? React with 👍 / 👎.

Comment thread openrag/api/main.py Outdated
app.add_middleware(InstrumentationMiddleware)
# Registered last among the app stack so it wraps outermost and stamps the
# baseline security headers on every response that flows out.
app.add_middleware(SecurityHeadersMiddleware)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ensure 500 errors get security headers

For exceptions handled by the generic Exception handler, Starlette routes the response through its outer ServerErrorMiddleware, which wraps all user-added middleware rather than passing back through this SecurityHeadersMiddleware. As a result, an unexpected route failure still returns the app's JSON 500 without the new baseline headers; cover this path by applying the headers in the 500 handler or with an ASGI wrapper outside Starlette's error middleware.

Useful? React with 👍 / 👎.

Addresses review feedback on the SecurityHeadersMiddleware coverage:

- Register the middleware after CORS (outermost) so it also stamps CORS
  preflight responses.
- Add the same middleware to the standalone Chainlit app so the Ray Serve
  Chainlit origin gets the baseline headers.
- Apply the headers in the unhandled-500 handler, since Starlette generates
  those responses in the outer ServerErrorMiddleware, outside the user stack.

Header logic is extracted into a shared apply_security_headers() used by both
the middleware and the 500 handler. Adds a test for the 500 path.
@andyne13

andyne13 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all three verified as valid and addressed in the latest commit:

  • Preflights bypass the middleware: SecurityHeadersMiddleware is now registered after CORS (outermost), so it wraps CORS preflight responses too.
  • Standalone Chainlit app: added the same middleware to chainlit_api.py, so the Ray-Serve Chainlit origin gets the baseline headers (in embedded/compose mode Chainlit is mounted on the main app and was already covered).
  • Unhandled 500s: Starlette generates those in the outer ServerErrorMiddleware, outside the user stack, so the 500 handler now sets the headers itself. Handled errors (400/403/OpenRAGError) already got them via ExceptionMiddleware inside the stack.

The header logic is extracted into a shared apply_security_headers() used by both the middleware and the 500 handler, and there's a new test asserting a 500 response carries the headers.

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one small but real gap before I can approve.

Normal responses from the standalone Chainlit app get the new headers, but unhandled 500 responses can still bypass them. The main API has the extra 500-handler path that calls apply_security_headers(), but chainlit_api.py only adds the middleware.

Simple example: if /chainlit/ raises unexpectedly in Ray Serve mode, that standalone origin can still return a 500 without X-Content-Type-Options, X-Frame-Options, or Referrer-Policy.

Could we add the same 500-header path for the standalone Chainlit app, or a small Chainlit-specific exception handler that calls apply_security_headers()?

Register the shared error handlers on the standalone Chainlit app so its
unhandled-500 responses (produced by Starlette's outer ServerErrorMiddleware,
outside the user middleware stack) get the same baseline security headers via
apply_security_headers. Also gives the Chainlit app consistent OpenRAGError/500
shaping.
@andyne13

andyne13 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — verified and fixed in 14d1b67.

You're right: the standalone Chainlit app had the middleware but no 500-handler path, so its unhandled-500s (generated by Starlette's outer ServerErrorMiddleware, outside the user stack) came back without the headers — same class as the main-app 500 case, which I'd only covered on api.main.

Fix: register_error_handlers(app) on the Chainlit app, so its 500s go through the same handler that calls apply_security_headers(). As a bonus the Chainlit app (which had no error handling before) now gets consistent OpenRAGError/500 shaping too. ruff + layer-import + the chainlit/security-headers tests pass.

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked the latest update. The previous gaps around CORS preflights, standalone Chainlit, and unhandled 500 responses are covered now, and CI plus the targeted local checks are green. Looks good to me.

@andyne13
andyne13 merged commit 123607e into refactor/hexagonal Jul 3, 2026
6 checks passed
@andyne13
andyne13 deleted the fix/pre-release-hardening branch July 3, 2026 16:24
@Ahmath-Gadji Ahmath-Gadji added the fix Fix issue label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Fix issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants