Repository navigation
fix(api): drop CORS credentials + skip same-origin decoration - #123
Conversation
WaveHouse is a Bearer-token API — cookies are never used and the previous middleware combination of `Access-Control-Allow-Credentials: true` with `Access-Control-Allow-Origin: *` was rejected by browsers per the CORS spec, silently breaking any client that set `credentials: 'include'`. Three behavior changes: 1. Drop `Allow-Credentials` entirely. Authorization: Bearer is an explicit request header, not a browser-managed cookie; credentials mode is unnecessary. Removes the spec violation and shrinks the CSRF surface (issue #30). 2. Requests with no Origin header (same-origin, server-to-server, curl) skip CORS decoration entirely instead of unconditionally stamping Allow-Methods/Allow-Headers on every response. 3. Disallowed-origin preflights still return 204 but with no CORS headers, which the browser treats as preflight failure — same outcome as before, but the methods/headers list no longer leaks to origins not on the allowlist. Tests pin each branch including a table-driven assertion that Allow-Credentials is never emitted across wildcard / empty-allowlist / allowlist-hit. Closes #29. Closes #30.
Summary of ChangesHello, 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 refines the API's CORS middleware to align with security best practices and technical specifications. By removing credential-related headers and optimizing header decoration for same-origin requests, the changes prevent potential spec violations and reduce the attack surface. The update also clarifies the intended use of the CORS allowlist through updated documentation and robust unit tests. Highlights
Using Gemini Code AssistThe 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
Customization To customize the 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 Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. 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. Footnotes
|
|
0 [MUST], 0 [SHOULD], 1 [MAY] — see the inline thread for detail. The CORS rewrite is correct and complete. Quick rundown of what I checked:
Ship it — fix the one-word CHANGELOG typo (already suggested inline) and merge. |
Claude review on PR #123 flagged that the CHANGELOG entry's file list referenced docs/development.md, but the actual edit was in docs/deployment.md. No code change.
There was a problem hiding this comment.
Code Review
This pull request refactors the CORS middleware to improve spec compliance and security by removing the Access-Control-Allow-Credentials header, skipping CORS decoration for same-origin requests, and short-circuiting preflights for disallowed origins. Documentation and tests have been updated to reflect these changes. Feedback focuses on ensuring these architectural shifts are recorded in AGENTS.md, improving cache consistency by sending the Vary: Origin header even for disallowed origins, and utilizing t.Context() in tests for better lifecycle management.
|
1 [MUST], 0 [SHOULD], 0 [MAY] — see the inline thread for detail. The CORS rewrite is correct and well-tested. The implementation ( One issue was missed in my first pass ( Iterate — truncate the CORS CHANGELOG entry at the dev-recipe sentence and restore the coverage-badge bullet as its own line. (Supersedes my earlier Ship-it verdict at #issuecomment-4435405141, which reviewed commit |
Two Gemini findings on PR #123: - [SHOULD] allowlist mode now sets Vary: Origin unconditionally — not just when the origin matches. Without Vary, a shared cache could memoize the headerless reject response under the URL alone and replay it to a later allowed-origin request, stripping the CORS headers and breaking the legitimate client. Added explanatory comment + a test assertion in TestCORSMiddleware_BlockedOrigin. - [MUST] AGENTS.md §Key Design Decisions gains item 15: the Bearer-token-only CORS posture (no Allow-Credentials, no cookies, no session middleware) is now an explicit architectural decision documented alongside the auth/policy/SDK decisions, so future contributors don't reintroduce Allow-Credentials or cookie-based auth without a design discussion. (Pushing back on the third comment — t.Context() in tests — in the inline reply: the prevailing convention across 65 callsites in internal/api/*_test.go is context.Background(), changing one or two creates inconsistency rather than reducing it.)
|
1 [MUST], 0 [SHOULD], 0 [MAY] — see the inline thread for detail. The implementation at HEAD ( The sole outstanding item is the garbled Iterate — apply the one-line CHANGELOG suggestion and this is done. |
Claude re-flagged on b9de0a9: my initial Edit replaced only the title of the existing "Coverage badge step…" bullet, leaving its tail (starting with "(.testcoverage.yml, AGENTS.md)…") spliced onto the end of the new CORS bullet — making the CORS entry look like it owned the coverage-badge fix and erasing the coverage-badge record from [Unreleased]. Restored the two bullets as separate entries: - CORS middleware (this PR — also updated to mention AGENTS.md in the file list and the Vary: Origin / item-15 additions from b9de0a9) - Coverage badge step (verbatim from main, restored) No code change.
|
0 [MUST], 0 [SHOULD], 0 [MAY] The Full implementation summary for the record:
Ship it — ready to merge. (Supersedes Iterate verdict at #issuecomment-4435483441.) |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughWaveHouse CORS middleware refactored to enforce a Bearer-token-only authentication posture by removing the ChangesCORS Bearer-Token-Only Enforcement
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
0 [MUST], 0 [SHOULD], 0 [MAY] Re-review of HEAD Everything from the prior The merge of main brought in no conflicting changes to the CORS path. All prior inline threads are resolved. Ship it — ready to merge. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/api/router_test.go`:
- Around line 184-200: The TestCORSMiddleware_BlockedOriginPreflight currently
only asserts Access-Control-Allow-Origin is empty; update the test
(TestCORSMiddleware_BlockedOriginPreflight) that uses corsMiddleware to also
assert that all other CORS response headers are absent/empty — e.g. assert.Empty
for "Access-Control-Allow-Methods", "Access-Control-Allow-Headers",
"Access-Control-Allow-Credentials" (and optionally "Access-Control-Max-Age") in
addition to the existing assertions so the test pins the full "no CORS headers"
contract.
🪄 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 Plus
Run ID: f5b3ef91-3ef2-4c0f-8ad4-3f90034fce22
📒 Files selected for processing (7)
AGENTS.mdCHANGELOG.mdconfig.yamldocs/src/content/docs/configuration.mddocs/src/content/docs/deployment.mdinternal/api/router.gointernal/api/router_test.go
CodeRabbit nitpick on the post-merge HEAD: the existing TestCORSMiddleware_BlockedOriginPreflight only checked Access-Control-Allow-Origin, so a regression that leaked Allow-Methods / Allow-Headers / Expose-Headers / Max-Age to a disallowed origin would slip through. Add positive assertions that all of those headers are empty AND that Allow-Credentials is never set (covers a different invariant than the dedicated NoCredentialsHeader test, since that one only exercises the allowed-origin paths). Also pin Vary: Origin as present on the reject path — it's set unconditionally in allowlist mode so a shared cache can't replay this headerless 204 to a later allowed-origin preflight.
|
0 [MUST], 0 [SHOULD], 0 [MAY] Re-review of HEAD Quick summary of what I verified:
Ship it — CodeRabbit's CHANGES_REQUESTED is resolved; no remaining issues. |
…wildcard # Conflicts: # AGENTS.md # CHANGELOG.md
|
0 [MUST], 0 [SHOULD], 0 [MAY] — no inline threads. Ship it — CORS spec-compliance fix is correct, |
taitelee
left a comment
There was a problem hiding this comment.
Good. Good catch on context life cycle.
Summary
Access-Control-Allow-Credentials: truefrom the API CORS middleware (closes Protect CSRF (Cross-Site Request Forgery) #30 — WaveHouse is a Bearer-token API, never used cookies, so credentials mode is unneeded and the prior combination ofAllow-Credentials: true+Allow-Origin: *violated the CORS spec).Origin(closes CORS Setup (Cross-Origin Resource Sharing) #29's remaining gap — same-origin/server-to-server callers don't need stamped CORS headers).Allow-Methods/Allow-Headerslist to origins outside the allowlist.#29 and #30 were both already most-of-the-way done — config knob, env var, allowlist mode, JWT middleware, no cookies anywhere in the tree. This PR fills the remaining holes and documents the design decision in code, config, and docs so it doesn't drift later.
Why
Reading both issues against the current code:
corsMiddleware+config.Server.CORSAllowedOrigins+ env varWH_SERVER_CORS_ALLOWED_ORIGINS; bug:Allow-Credentials: truepaired withAllow-Origin: *is a spec violation browsers rejectJWTAuthMiddlewarereadsAuthorization: Bearer …only; nohttp.Cookie/SetCookieanywhere ininternal/orclients/ts/src/; SDK never setscredentials: 'include'The credentials fix has two motivations: (a) it removes the spec violation, and (b) it explicitly closes the door on future cookie-based auth being added without thought, which would re-introduce the CSRF surface #30 is trying to keep closed.
What changed
internal/api/router.go—corsMiddlewarerewritten with explicit policy: no Origin → passthrough; wildcard → echo*; allowlist hit → echo origin +Vary: Origin; allowlist miss → no headers (preflight still gets 204). Credentials header dropped across all branches.internal/api/router_test.go— existing cases preserved + table-drivenTestCORSMiddleware_NoCredentialsHeader(wildcard, empty-allowlist, allowlist-hit) +TestCORSMiddleware_NoOriginIsPassthrough+TestCORSMiddleware_BlockedOriginPreflight.config.yaml— comment block explaining the Bearer-token rationale + dev recipe (http://localhost:3000).docs/configuration.md,docs/deployment.md— note the credentials decision so operators don't try to "fix" it by re-adding the header.CHANGELOG.md—[Unreleased] / Fixedentry.Test plan
make verify(tidy + fmt + vulncheck + lint) — cleanmake test-unit— 451 tests, unit coverage 73.4% (gate: 70%)make ci— full pipeline (unit + integration + sdk + e2e + merged 80% gate) all greeninternal/api/stream_ws.go— WS handler's ownOriginPatternsallowlist agrees with the middleware policyCookie/SetCookieacrossinternal/andclients/— none found, confirming the no-cookies invariantSummary by CodeRabbit
Bug Fixes
Documentation
Tests