Skip to content

fix: ensure security headers on all HTTP responses#437

Merged
Aureliolo merged 5 commits intomainfrom
fix/zap-scan-findings
Mar 15, 2026
Merged

fix: ensure security headers on all HTTP responses#437
Aureliolo merged 5 commits intomainfrom
fix/zap-scan-findings

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Replace Litestar response_headers + CSPMiddleware with a single before_send hook (security_headers_hook) that fires for ALL HTTP responses — including exception-handler responses (400, 500) and router-level 404/405 that previously had no security headers
  • Add Cross-Origin-Opener-Policy: same-origin to the static security headers set
  • Add base-uri 'self' to both CSP policies to prevent <base> tag injection
  • Use headers[name] = value (overwrite) instead of headers.add() (append) to prevent duplicate headers if any middleware sets the same header
  • Document unsafe-inline in docs CSP as an accepted Scalar UI constraint

Why before_send?

Litestar's before_send hook wraps the ASGI send callback at the outermost layer (Litestar.__call__), before the middleware stack. User-defined ASGI middleware only runs for matched routes — 404 and 405 responses from the router bypass it entirely. response_headers only applies to successful route handler responses, not exception handler responses.

Before/After

Response Before After
200 health All headers All headers
400 malformed JSON Missing 7 headers All headers
404 unmatched route Missing ALL headers All headers
405 wrong method Missing ALL headers All headers
500 server error Missing 7 headers All headers

Closes #355

Test plan

  • New TestSecurityHeadersHook class verifies headers on 200, 400, 404, 405, 500 responses
  • New test_non_http_scope_is_skipped verifies WebSocket/lifespan scopes are not modified
  • CSP path boundary tests verify strict/relaxed CSP selection
  • All tests assert both static security headers AND CSP presence
  • Docker spot-check confirmed all 7 curl scenarios pass
  • Full test suite: 7995 passed, 94.49% coverage
  • Pre-reviewed by 5 agents, 7 findings addressed

🤖 Generated with Claude Code

Litestar's response_headers only applies to successfully handled
routes — exception-handler responses (400, 500) and router-level
404/405 responses were missing all security headers (CORP, HSTS,
X-Content-Type-Options, etc.).

Replace response_headers + CSPMiddleware with a single before_send
hook (security_headers_hook). Litestar's before_send wraps the ASGI
send callback at the outermost layer, so it fires for ALL responses.

Closes #355
Address review agent findings:
- Switch headers.add() to headers[name]=value to prevent duplicates
- Add base-uri 'self' to both CSP policies (prevents base tag injection)
- Add Cross-Origin-Opener-Policy: same-origin to static headers
- Document unsafe-inline in docs CSP as accepted Scalar UI constraint
- Add Args section to security_headers_hook docstring
- Add CSP assertion to all response-type tests
- Add test for non-HTTP scope passthrough

Pre-reviewed by 5 agents, 7 findings addressed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 15, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0ea0098-340d-4af6-9bb7-ef50e5a1df23

📥 Commits

Reviewing files that changed from the base of the PR and between c68a656 and 8438493.

📒 Files selected for processing (1)
  • src/synthorg/api/middleware.py
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Greptile Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Read the relevant docs/design/ page before implementing any feature or planning any issue. The design spec is the starting point for architecture, data models, and behavior.

Files:

  • src/synthorg/api/middleware.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: No from __future__ import annotations — Python 3.14+ has PEP 649 native lazy annotations.
Use except A, B: syntax (no parentheses) — ruff enforces PEP 758 except syntax for Python 3.14.
Add type hints to all public functions. Use mypy strict mode.
Write Google-style docstrings on all public classes and functions. Enforced by ruff D rules.
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
Use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence) for dict/list fields in frozen Pydantic models.
Use frozen Pydantic models for config/identity. Use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields.
Use @computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr for all identifier/name fields (including optional and tuple variants).
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code. Prefer structured concurrency over bare create_task.
Keep functions under 50 lines and files under 800 lines.
Handle errors explicitly, never silently swallow them.
Validate at system boundaries (user input, external APIs, config files).
Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(__name__). Use event name constants from synthorg.observability.events.<domain>.
Never use import logging, logging.getLogger(), or print() in application code.
Always use the variable name logger (not _logger, not log).
Use structured logging: logger.info(EVENT, key=value) — never logger.info("msg %s", val). Log at WARNING or ERROR with context before raising. Log all state t...

Files:

  • src/synthorg/api/middleware.py
🧠 Learnings (3)
📚 Learning: 2026-03-15T01:53:24.084Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T01:53:24.084Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence) for `dict`/`list` fields in frozen Pydantic models.

Applied to files:

  • src/synthorg/api/middleware.py
📚 Learning: 2026-03-15T01:53:24.084Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T01:53:24.084Z
Learning: Applies to src/synthorg/**/*.py : Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction + `MappingProxyType` wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/api/middleware.py
📚 Learning: 2026-03-15T01:53:24.084Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T01:53:24.084Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity. Use separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state that evolves. Never mix static config fields with mutable runtime fields.

Applied to files:

  • src/synthorg/api/middleware.py
🔇 Additional comments (3)
src/synthorg/api/middleware.py (3)

61-72: LGTM — immutable security headers correctly implemented.

The MappingProxyType wrapping ensures runtime immutability. Since the dict literal is created inline and immediately wrapped (no external references), the copy.deepcopy() from the previous suggestion is unnecessary here.


75-114: LGTM — well-structured before_send hook with correct semantics.

The implementation correctly:

  • Guards non-HTTP scopes and non-response.start messages with early returns
  • Uses __setitem__ (overwrite) to prevent duplicate headers
  • Handles path-aware CSP selection with proper boundary matching (/docs and /docs/ prefix)
  • Documents the COOP relaxation rationale for Scalar UI OAuth flows

36-58: LGTM — CSP policies include recommended security directives.

Both policies correctly include:

  • object-src 'none' — blocks plugin injection (addresses OWASP CSP evaluator findings per commit message)
  • base-uri 'self' — prevents base tag hijacking
  • frame-ancestors 'none' — clickjacking protection

The relaxed /docs CSP appropriately documents the 'unsafe-inline' risk as an accepted tradeoff for Scalar UI functionality.


📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Security headers are now applied globally before responses are sent, ensuring consistent headers on all HTTP responses (including errors and unmatched routes).
    • API endpoints receive a stricter content-security policy; documentation pages receive a relaxed policy plus an adjusted cross-origin opener policy.
  • Tests
    • Test suite updated to validate security headers across status codes, routes, and docs vs API paths.

Walkthrough

Replaces class-based CSPMiddleware with a global before_send hook security_headers_hook and an immutable _SECURITY_HEADERS mapping; app configuration now applies the hook instead of middleware and tests updated to assert security headers across routes and error handlers.

Changes

Cohort / File(s) Summary
App configuration
src/synthorg/api/app.py
Removed CSPMiddleware import and its inclusion in middleware; swapped explicit response_headers for registering security_headers_hook as a before_send hook.
Middleware & security headers
src/synthorg/api/middleware.py
Removed CSPMiddleware class. Added _SECURITY_HEADERS (immutable MappingProxyType), path-aware CSP constants (_API_CSP, _DOCS_CSP), and async def security_headers_hook(message: Message, scope: Scope) which overwrites response headers using MutableScopeHeaders and applies COOP for docs paths.
Tests
tests/unit/api/test_app.py, tests/unit/api/test_middleware.py
Updated tests to import _SECURITY_HEADERS and exercise security_headers_hook across success, client, server, docs vs API paths, and non-HTTP scopes; removed tests that invoked the removed CSPMiddleware class directly.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant App as "Litestar App"
  participant Handler as "Route / Exception Handler"
  participant Hook as "security_headers_hook"
  participant Response

  Client->>App: HTTP request
  App->>Handler: dispatch request
  Handler-->>App: response / exception
  App->>Hook: before_send(message, scope)
  Hook-->>App: write/overwrite security headers (choose CSP by path, add COOP for /docs)
  App-->>Response: finalize ASGI response
  Response-->>Client: HTTP response with security headers
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: ensure security headers on all HTTP responses' clearly and concisely describes the main change: implementing a mechanism to apply security headers universally across all HTTP response types.
Description check ✅ Passed The description is directly related to the changeset, providing detailed rationale, before/after comparison, and testing approach for the security headers implementation.
Linked Issues check ✅ Passed The PR successfully addresses issue #355 by ensuring security headers are applied to all HTTP responses (200, 400, 404, 405, 500), eliminating gaps where headers were previously missing on router-level and exception-handler responses.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the security_headers_hook and refactoring security header handling as required by issue #355; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/zap-scan-findings
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/zap-scan-findings
📝 Coding Plan
  • Generate coding plan for human review comments

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

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's security posture by centralizing and standardizing the application of security headers. By migrating from disparate Litestar mechanisms to a unified before_send hook, it guarantees that critical security policies, including Content-Security-Policy and various static headers, are present on all HTTP responses, regardless of whether they are successful, error-handling, or unmatched routes. This change closes a security gap where certain error responses previously lacked essential protections.

Highlights

  • Unified Security Header Application: Replaced Litestar's response_headers and CSPMiddleware with a single before_send hook (security_headers_hook). This ensures all HTTP responses, including those from exception handlers (400, 500) and router-level 404/405 errors, consistently receive security headers, which was not previously the case.
  • Enhanced Security Policies: Added Cross-Origin-Opener-Policy: same-origin to the static security headers and base-uri 'self' to both strict and relaxed Content-Security-Policy (CSP) definitions to prevent <base> tag injection.
  • Header Overwrite Logic: Implemented header setting using headers[name] = value instead of headers.add() within the security_headers_hook to prevent duplicate headers if any middleware or handler attempts to set the same header.
  • Improved Test Coverage: Introduced new comprehensive tests in TestSecurityHeadersHook to verify security headers across various HTTP response types (200, 400, 404, 405, 500) and confirmed that non-HTTP scopes (like WebSockets) are correctly skipped. CSP path boundary tests were also updated to reflect the new implementation.
Changelog
  • src/synthorg/api/app.py
    • Removed ResponseHeader and CSPMiddleware imports.
    • Imported security_headers_hook.
    • Replaced the response_headers configuration with before_send=[security_headers_hook].
    • Removed CSPMiddleware from the application's middleware list.
  • src/synthorg/api/middleware.py
    • Updated module docstring to describe the new before_send hook approach for security headers.
    • Imported MutableScopeHeaders and Message.
    • Modified _API_CSP and _DOCS_CSP to include base-uri 'self' directive.
    • Defined _SECURITY_HEADERS dictionary containing all static security headers, including the new Cross-Origin-Opener-Policy.
    • Removed the CSPMiddleware class.
    • Added the security_headers_hook asynchronous function to apply static security headers and path-aware CSP to HTTP responses using MutableScopeHeaders and overwrite logic.
  • tests/unit/api/test_app.py
    • Imported _SECURITY_HEADERS for consistent testing.
    • Updated test_security_response_headers to use the _SECURITY_HEADERS dictionary for parameterization, ensuring all defined static headers are tested.
  • tests/unit/api/test_middleware.py
    • Updated module docstring to reflect changes from middleware to a hook.
    • Removed _fake_app and _make_scope helper functions.
    • Added _make_app helper to create a minimal Litestar application for testing.
    • Added _assert_all_security_headers helper to verify the presence of all security headers.
    • Introduced TestSecurityHeadersHook class with tests for security headers on 200, 400, 404, 405, and 500 responses.
    • Added a test to TestSecurityHeadersHook to confirm non-HTTP scopes are skipped.
    • Refactored TestCSPPathSelection to use TestClient and _make_app for verifying CSP path boundaries.
Activity
  • The pull request description indicates that the changes were pre-reviewed by 5 agents, and 7 findings were addressed.
  • A comprehensive test plan was executed, including new tests for security headers on various response types (200, 400, 404, 405, 500), non-HTTP scope handling, and CSP path boundary selection.
  • The full test suite passed with 7995 tests and 94.49% coverage, indicating thorough validation.
  • Docker spot-checks confirmed all 7 curl scenarios passed, validating the changes in a deployed environment.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 15, 2026 11:03 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a great improvement for security, centralizing the logic for security headers into a before_send hook to ensure they are applied to all HTTP responses, including errors. The implementation is clean and the new tests are comprehensive. I've found one area for improvement in the Content-Security-Policy definitions to make them even more robust against clickjacking.


# Strict CSP for API routes — no inline scripts, self-origin only.
_API_CSP: Final[str] = "default-src 'self'; script-src 'self'"
_API_CSP: Final[str] = "default-src 'self'; script-src 'self'; base-uri 'self'"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The current Content-Security-Policy (CSP) for API routes is missing the frame-ancestors directive. While X-Frame-Options: DENY is set, modern browsers prefer the frame-ancestors CSP directive and will ignore X-Frame-Options if a CSP is present. Without frame-ancestors, the policy does not restrict framing, which contradicts the intent to prevent clickjacking. To ensure consistent protection, you should add frame-ancestors 'none' to the policy.

Suggested change
_API_CSP: Final[str] = "default-src 'self'; script-src 'self'; base-uri 'self'"
_API_CSP: Final[str] = "default-src 'self'; script-src 'self'; base-uri 'self'; frame-ancestors 'none'"

Comment on lines 44 to 52
_DOCS_CSP: Final[str] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"img-src 'self' data: https://cdn.jsdelivr.net; "
"font-src 'self' data: https://cdn.jsdelivr.net https://fonts.scalar.com; "
"connect-src 'self' https://cdn.jsdelivr.net https://proxy.scalar.com"
"connect-src 'self' https://cdn.jsdelivr.net https://proxy.scalar.com; "
"base-uri 'self'"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Similar to the API CSP, the CSP for the documentation pages is missing the frame-ancestors directive. This could allow the documentation site to be framed by malicious pages, potentially leading to clickjacking attacks. To prevent this, you should add frame-ancestors 'none' to this policy as well.

Suggested change
_DOCS_CSP: Final[str] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"img-src 'self' data: https://cdn.jsdelivr.net; "
"font-src 'self' data: https://cdn.jsdelivr.net https://fonts.scalar.com; "
"connect-src 'self' https://cdn.jsdelivr.net https://proxy.scalar.com"
"connect-src 'self' https://cdn.jsdelivr.net https://proxy.scalar.com; "
"base-uri 'self'"
)
_DOCS_CSP: Final[str] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"img-src 'self' data: https://cdn.jsdelivr.net; "
"font-src 'self' data: https://cdn.jsdelivr.net https://fonts.scalar.com; "
"connect-src 'self' https://cdn.jsdelivr.net https://proxy.scalar.com; "
"base-uri 'self'; "
"frame-ancestors 'none'"
)

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR replaces Litestar's response_headers + the old CSPMiddleware ASGI class with a single before_send hook (security_headers_hook) that fires for every HTTP response, closing the security-header gap on exception-handler responses (400, 500) and router-level 404/405 that previously received no headers at all. It also hardens both CSP policies (object-src 'none', base-uri 'self') and adds Cross-Origin-Opener-Policy: same-origin globally with a targeted unsafe-none relaxation for /docs/ paths to accommodate Scalar UI.

Key changes:

  • security_headers_hook correctly guards on both scope["type"] != ScopeType.HTTP and message["type"] != "http.response.start", is idempotent via overwrite semantics, and uses an immutable MappingProxyType for the static header set
  • _DOCS_CSP and _API_CSP both now include object-src 'none' and base-uri 'self'; COOP is overridden to unsafe-none for docs paths only
  • TestSecurityHeadersHook provides direct evidence that all 5 response categories (200, 400, 404, 405, 500) carry the full header set
  • test_docs_path_relaxes_coop and test_api_path_keeps_strict_coop close the gap from the previous review round
  • test_security_response_headers in test_app.py now derives its parametrize cases from _SECURITY_HEADERS directly, so it stays in sync automatically if the header set changes

Confidence Score: 5/5

  • Safe to merge — the hook is correct, well-tested across all response categories, and all previous review findings have been fully addressed.
  • The implementation is architecturally sound: before_send is the correct Litestar primitive for this problem (fires before the middleware stack for all HTTP responses), the guard conditions are correct, header overwrite semantics prevent duplicates, and the MappingProxyType prevents accidental runtime mutation of the header map. Both CSP policies have been hardened with object-src 'none' and base-uri 'self'. The COOP path-awareness for /docs/ is tested explicitly. Test coverage spans all five targeted response types (200, 400, 404, 405, 500) plus non-HTTP scope skipping and CSP path boundary cases. No new issues found beyond those addressed by previous review rounds.
  • No files require special attention.

Important Files Changed

Filename Overview
src/synthorg/api/middleware.py Replaces CSPMiddleware ASGI class with a before_send hook; adds COOP with docs-path relaxation to unsafe-none, object-src 'none' and base-uri 'self' to both CSPs, and immutable _SECURITY_HEADERS map. Implementation is correct and well-documented.
src/synthorg/api/app.py Cleanly swaps response_headers list + CSPMiddleware for before_send=[security_headers_hook]; removes ResponseHeader and CSPMiddleware imports. No issues found.
tests/unit/api/test_middleware.py Replaces direct ASGI invocation tests with TestClient-based integration tests covering 200/400/404/405/500 responses, plus COOP path-aware assertions. One async def method among sync siblings (known, asyncio_mode=auto handles it). test_csp_path_boundary boundary paths /documents and /docsearch return 404 from the full app, but the test only asserts CSP which is correctly path-routed on all responses.
tests/unit/api/test_app.py test_security_response_headers parametrized test now derives its cases from _SECURITY_HEADERS directly and hits /api/v1/health instead of /docs to avoid COOP relaxation; clean and correct.

Last reviewed commit: 8438493

Comment on lines +115 to +126
async def test_non_http_scope_is_skipped(self) -> None:
"""Non-HTTP scopes (WebSocket, lifespan) are not modified."""
message: Any = {
"type": "websocket.connect",
"headers": [],
}
)
await send({"type": "http.response.body", "body": b""})
scope: Any = {"type": ScopeType.WEBSOCKET}

await security_headers_hook(message, scope)

def _make_scope(path: str) -> dict[str, Any]:
"""Build a minimal ASGI HTTP scope for testing."""
return {
"type": "http",
"path": path,
"method": "GET",
"headers": [],
"query_string": b"",
"root_path": "",
"scheme": "http",
"server": ("localhost", 8000),
}
# Headers list should remain empty — hook returned early.
assert message.get("headers") == []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

async def test without @pytest.mark.asyncio

test_non_http_scope_is_skipped is an async def method in TestSecurityHeadersHook, while all sibling tests are synchronous def methods. This works because asyncio_mode = "auto" is configured in pyproject.toml, but it's inconsistent with the class and makes the async nature implicit.

Since security_headers_hook is an async function (the await on line 123 is necessary), the test must remain async. Consider adding an explicit @pytest.mark.asyncio decorator for clarity, or document in a comment why this one method is async while the others are not.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/api/test_middleware.py
Line: 115-126

Comment:
**`async def` test without `@pytest.mark.asyncio`**

`test_non_http_scope_is_skipped` is an `async def` method in `TestSecurityHeadersHook`, while all sibling tests are synchronous `def` methods. This works because `asyncio_mode = "auto"` is configured in `pyproject.toml`, but it's inconsistent with the class and makes the async nature implicit.

Since `security_headers_hook` is an `async` function (the `await` on line 123 is necessary), the test must remain `async`. Consider adding an explicit `@pytest.mark.asyncio` decorator for clarity, or document in a comment why this one method is async while the others are not.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Address external reviewer feedback (Gemini, Greptile):
- Add frame-ancestors 'none' to both API and docs CSP policies
  (browsers ignore X-Frame-Options when CSP is present)
- Make COOP path-aware: relax to unsafe-none on /docs paths to
  avoid breaking Scalar UI cross-origin popup features
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/middleware.py`:
- Around line 16-17: The global _SECURITY_HEADERS dict is declared Final but
remains mutable; replace its mutable dict with an immutable mapping by
constructing it via a deepcopy of the original literal and wrapping with
MappingProxyType, update the annotation to Final[Mapping[str, str]] (or
types.MappingProxyType) and add required imports (Mapping/MappingProxyType and
deepcopy) so _SECURITY_HEADERS cannot be mutated at runtime; locate the
definition named _SECURITY_HEADERS in this module and apply the wrap and type
change accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7f4eaff8-bda3-4f00-aeb6-f3ec0a1411df

📥 Commits

Reviewing files that changed from the base of the PR and between 3f64a2e and 61c4922.

📒 Files selected for processing (2)
  • src/synthorg/api/middleware.py
  • tests/unit/api/test_app.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Backend
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Read the relevant docs/design/ page before implementing any feature or planning any issue. The design spec is the starting point for architecture, data models, and behavior.

Files:

  • tests/unit/api/test_app.py
  • src/synthorg/api/middleware.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Mark all tests with pytest markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, or @pytest.mark.slow.
Maintain 80% minimum code coverage. Always run pytest with -n auto for parallel execution via pytest-xdist. Set test timeout to 30 seconds per test.
Use @pytest.mark.parametrize for testing similar cases instead of multiple test functions.
Use Hypothesis for property-based testing with @given + @settings. Profiles: ci (200 examples, default) and dev (1000 examples), controlled via HYPOTHESIS_PROFILE env var. Run dev profile: HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n auto -k properties.
NEVER use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in tests. Use generic names: test-provider, test-small-001, test-medium-001, test-large-001, or aliases small/medium/large.

Files:

  • tests/unit/api/test_app.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: No from __future__ import annotations — Python 3.14+ has PEP 649 native lazy annotations.
Use except A, B: syntax (no parentheses) — ruff enforces PEP 758 except syntax for Python 3.14.
Add type hints to all public functions. Use mypy strict mode.
Write Google-style docstrings on all public classes and functions. Enforced by ruff D rules.
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
Use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence) for dict/list fields in frozen Pydantic models.
Use frozen Pydantic models for config/identity. Use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields.
Use @computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr for all identifier/name fields (including optional and tuple variants).
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code. Prefer structured concurrency over bare create_task.
Keep functions under 50 lines and files under 800 lines.
Handle errors explicitly, never silently swallow them.
Validate at system boundaries (user input, external APIs, config files).
Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(__name__). Use event name constants from synthorg.observability.events.<domain>.
Never use import logging, logging.getLogger(), or print() in application code.
Always use the variable name logger (not _logger, not log).
Use structured logging: logger.info(EVENT, key=value) — never logger.info("msg %s", val). Log at WARNING or ERROR with context before raising. Log all state t...

Files:

  • src/synthorg/api/middleware.py
🔇 Additional comments (2)
src/synthorg/api/middleware.py (1)

90-109: Hook coverage and overwrite semantics are correctly implemented.

The Line 90/92 guards and Line 98-Line 109 overwrite strategy are aligned with the goal of consistent headers on all HTTP response starts, including error paths.

tests/unit/api/test_app.py (1)

36-48: Good test targeting for static-header assertions.

Using a non-docs endpoint at Line 47 avoids the docs-specific COOP relaxation and keeps this parameterized check focused on baseline security headers.

Address round-2 reviewer feedback (CodeRabbit, Greptile):
- Wrap _SECURITY_HEADERS with MappingProxyType for runtime immutability
- Add tests for docs-path COOP relaxation (unsafe-none vs same-origin)
- Document _assert_all_security_headers as non-docs-path only
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.67%. Comparing base (440c540) to head (8438493).
⚠️ Report is 7 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #437      +/-   ##
==========================================
- Coverage   93.67%   93.67%   -0.01%     
==========================================
  Files         469      469              
  Lines       22214    22213       -1     
  Branches     2142     2144       +2     
==========================================
- Hits        20809    20808       -1     
  Misses       1094     1094              
  Partials      311      311              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Prevents plugin injection via <object>/<embed>/<applet> elements.
Without explicit object-src, default-src 'self' allows same-origin
plugin embeds — flagged by OWASP and Google CSP evaluator.
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 15, 2026 11:42 — with GitHub Actions Inactive
@Aureliolo Aureliolo merged commit 837f2fc into main Mar 15, 2026
31 checks passed
@Aureliolo Aureliolo deleted the fix/zap-scan-findings branch March 15, 2026 11:47
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 15, 2026 11:47 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Mar 15, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.2.4](v0.2.3...v0.2.4)
(2026-03-15)


### Bug Fixes

* attach cosign signatures and provenance bundle to release assets
([#438](#438))
([f191a4d](f191a4d))
* create git tag explicitly for draft releases
([#432](#432))
([1f5120e](1f5120e))
* docker healthcheck, CI optimization, and container hardening
([#436](#436))
([4d32bca](4d32bca))
* ensure security headers on all HTTP responses
([#437](#437))
([837f2fc](837f2fc))
* make install scripts usable immediately without terminal restart
([#433](#433))
([b45533c](b45533c))
* migrate pids_limit to deploy.resources.limits.pids
([#439](#439))
([66b94fd](66b94fd))


### Refactoring

* redesign release notes layout
([#434](#434))
([239aaf7](239aaf7))


### Maintenance

* **site:** replace hero CTA with license link and scroll arrow
([#440](#440))
([56af41c](56af41c))
* **web:** adopt @vue/tsconfig preset
([#435](#435))
([7d4b214](7d4b214))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Mar 15, 2026
Aureliolo added a commit that referenced this pull request Mar 15, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.2.4](v0.2.3...v0.2.4)
(2026-03-15)


### Bug Fixes

* attach cosign signatures and provenance bundle to release assets
([#438](#438))
([f191a4d](f191a4d))
* create git tag explicitly for draft releases
([#432](#432))
([1f5120e](1f5120e))
* docker healthcheck, CI optimization, and container hardening
([#436](#436))
([4d32bca](4d32bca))
* ensure security headers on all HTTP responses
([#437](#437))
([837f2fc](837f2fc))
* make install scripts usable immediately without terminal restart
([#433](#433))
([b45533c](b45533c))
* migrate pids_limit to deploy.resources.limits.pids
([#439](#439))
([66b94fd](66b94fd))
* use cosign --bundle flag for checksums signing
([#443](#443))
([19735b9](19735b9))


### Refactoring

* redesign release notes layout
([#434](#434))
([239aaf7](239aaf7))


### Maintenance

* **main:** release 0.2.4
([#431](#431))
([63b03c4](63b03c4))
* remove stale v0.2.4 changelog section from failed release
([#446](#446))
([769de10](769de10))
* reset version to 0.2.3 for re-release
([#444](#444))
([8579993](8579993))
* **site:** replace hero CTA with license link and scroll arrow
([#440](#440))
([56af41c](56af41c))
* **web:** adopt @vue/tsconfig preset
([#435](#435))
([7d4b214](7d4b214))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ZAP API Scan Report

1 participant