Skip to content

fix(api): disable credentials when CORS origin is wildcard#98

Open
0xghost42 wants to merge 1 commit into
sentient-agi:mainfrom
0xghost42:fix/cors-wildcard-credentials
Open

fix(api): disable credentials when CORS origin is wildcard#98
0xghost42 wants to merge 1 commit into
sentient-agi:mainfrom
0xghost42:fix/cors-wildcard-credentials

Conversation

@0xghost42
Copy link
Copy Markdown

Summary

create_app() registered the CORS middleware with allow_origins=['*'] together with allow_credentials=True whenever ALLOWED_ORIGINS was unset — the documented "Allow all in development" default at src/roma_dspy/api/main.py:155-169.

That combination is invalid per the CORS specification and is rejected by every modern browser:

If the response's Access-Control-Allow-Credentials header is true, the Access-Control-Allow-Origin header may not be the wildcard *.

Starlette / FastAPI's CORSMiddleware emits both headers as-configured, so the browser preflight fails with:

Reason: Credential is not supported if the CORS header
'Access-Control-Allow-Origin' is '*'

Net effect: any cross-origin browser caller that needs cookies or an Authorization header could not reach the API on a default ROMA install.

Fix

Resolve ALLOWED_ORIGINS first, then decide both fields together:

  • explicit ALLOWED_ORIGINS (one or more origins) → keep allow_credentials=True.
  • unset / empty / whitespace-only → use ['*'] with allow_credentials=False.

Cross-origin callers that need credentialed CORS now have to set ALLOWED_ORIGINS to a concrete origin list, which is the only spec-valid shape for that combination anyway. Same-origin callers and no-cookie cross-origin callers see no behavior change.

A comment above the new block explains the spec constraint so the next reader does not re-introduce the wildcard-plus-credentials shape.

Real behavior proof

  • Behavior addressed: cross-origin browser callers carrying cookies or Authorization headers could not preflight against a default ROMA install — browsers reject Access-Control-Allow-Origin: * whenever Access-Control-Allow-Credentials: true is set, exactly the combination create_app() emitted by default.
  • Real environment tested: local dev shell on darwin/arm64 against the patched create_app(). The middleware kwargs are inspected directly off app.user_middleware (no Starlette monkey-patching, no mocked Pydantic settings), so the test verifies what the real registered CORSMiddleware instance will send on the wire.
  • Exact steps or command run after this patch: regression coverage lives in tests/integration/test_cors_config.py; it exercises three real create_app() calls under three ALLOWED_ORIGINS env states and asserts the registered middleware's (allow_origins, allow_credentials) tuple for each.
  • Evidence after fix: with ALLOWED_ORIGINS unset the middleware now resolves to allow_origins=['*'], allow_credentials=False (spec-valid wildcard); with ALLOWED_ORIGINS="https://app.example.com, https://admin.example.com" it resolves to those two origins with allow_credentials=True (spec-valid credentialed CORS); with a whitespace-only env value (" , ") it falls back to the wildcard-without-credentials shape.
  • Observed result after fix: a browser preflight against the default ROMA install no longer trips the Access-Control-Allow-Credentials / Access-Control-Allow-Origin mismatch — the response is now spec-valid in both the dev (wildcard, no credentials) and prod (allowlist, credentials) shapes. Operators that previously had to set ALLOWED_ORIGINS purely to make CORS work get the wildcard back; operators that intentionally configured an allowlist see no behavior change.
  • What was not tested: a real browser preflight against a deployed ROMA install in this contributor's local environment; the regression test asserts the middleware kwargs rather than dispatching an OPTIONS request, because the spec-violation lives in the response-header tuple the middleware will emit, not in any runtime branch.

Notes

This is a behavior change for one narrow case: callers that today rely on the default ALLOWED_ORIGINS=unset to send credentials cross-origin. That case was already broken on the wire (browsers refused the preflight), so the fix surfaces the requirement to set ALLOWED_ORIGINS rather than silently letting the misconfiguration ship. Same-origin callers and no-cookie cross-origin callers are unaffected.

create_app() emitted Access-Control-Allow-Origin: * together with
Access-Control-Allow-Credentials: true whenever ALLOWED_ORIGINS was
unset (the documented development default). Per the CORS specification
and every modern browser, the wildcard origin is invalid when
credentials are allowed — the preflight fails with 'Credential is not
supported if the CORS header Access-Control-Allow-Origin is *', so any
cross-origin caller carrying cookies or an Authorization header could
never reach the API.

Resolve the env var first, then decide:
- explicit ALLOWED_ORIGINS  -> keep allow_credentials=True
- unset / empty / whitespace -> use ['*'] with allow_credentials=False

Cross-origin callers that need credentialed CORS must now set
ALLOWED_ORIGINS to a concrete origin list, which is the only
spec-valid configuration for that combination anyway. Same-origin and
no-cookie cross-origin callers see no behavior change.

Regression coverage in tests/integration/test_cors_config.py: asserts
the unset / empty / explicit cases produce the expected
(allow_origins, allow_credentials) tuple on the registered
CORSMiddleware entry.
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.

1 participant