diff --git a/CHANGELOG.md b/CHANGELOG.md index f6608ecf..22c34a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ - **PR #2170** by @franksong2702 — `/api/session?messages=0&resolve_model=0` metadata loads no longer pay the `_lookup_cli_session_metadata()` Agent/CLI scan for native WebUI sessions. New `_needs_cli_session_metadata()` predicate keeps the Agent metadata merge path for imported CLI sessions, messaging-backed sessions, read-only sessions, and external-agent sessions, but skips it for ordinary WebUI-native sessions. Profiling on real production state showed this was the remaining hot path after PR #2166 removed the duplicate browser post-render work. +### Stage-346 maintainer fixes + +- **`server.py` CSP-report auth carve-out scoped to POST only** — Opus SHOULD-FIX from stage-346 review on PR #2160. The original carve-out (`parsed.path != "/api/csp-report" and not check_auth`) bypassed auth for all write methods on the endpoint, not just the POST that browsers actually use for CSP violation reports. PATCH/DELETE to that path currently fall through to a 403 (CSRF check) or 404 (routing), so the broad bypass was harmless — but defense-in-depth says scope the carve-out to its actual use. New check: `parsed.path == "/api/csp-report" and self.command == "POST"`. ~6 LOC. CSP report regression suite (6 tests) still passes. + ## [v0.51.52] — 2026-05-12 — Release AB (stage-345 — 2-PR low-risk batch — stream-ownership guard + Refresh-usage button on provider quota card) ### Fixed diff --git a/server.py b/server.py index 25aa110e..c61d09d4 100644 --- a/server.py +++ b/server.py @@ -265,7 +265,15 @@ class Handler(BaseHTTPRequestHandler): set_request_profile(cookie_profile) try: parsed = urlparse(self.path) - if parsed.path != "/api/csp-report" and not check_auth(self, parsed): return + # Stage-346 Opus SHOULD-FIX defense-in-depth: scope the CSP-report + # auth carve-out to POST only. The endpoint is intentionally + # unauthenticated (browsers omit cookies on CSP reports), but the + # carve-out should not extend to PATCH/DELETE on that path even + # though they currently fail through CSRF/routing fallthrough. + _is_csp_report_post = ( + parsed.path == "/api/csp-report" and self.command == "POST" + ) + if not _is_csp_report_post and not check_auth(self, parsed): return result = route_func(self, parsed) if result is False: return j(self, {'error': 'not found'}, status=404)