feat(api): extend CORS_ORIGINS to support https://*.domain patterns #7
Labels
No labels
agent:implement
agent:ready
api
bot
bug
ci
enhancement
ready
security
terra-classic
testing
ux
web
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-ecosystem-legal#7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Extend API
build_cors/CORS_ORIGINSso operators can allow entire HTTPS subdomain trees (e.g.https://*.cl8y.com) alongside exact origins, without opening CORS to the entire internet (*).This unblocks browser
TermsGateclients on ecosystem dapps (e.g.https://dex.cl8y.com) that currently fail with Unable to verify terms acceptance: Failed to fetch because the production allowlist only reflectshttps://terms.cl8y.com.Current codebase
How CORS works today
CORS_ORIGINSis a comma-separated list of strings loaded inapi/src/config.rs(defaulthttps://terms.cl8y.com).api/src/routes/mod.rsbuild_cors:["*"]→CorsLayerwithallow_origin(Any)(fully open).HeaderValueand passed toallow_origin(origins).*.cl8y.comorhttps://*.cl8y.comwill not match browserOrigin: https://dex.cl8y.com.packages/cl8y-clickwrap) callGET {apiBaseUrl}/api/v1/signatures/statuscross-origin. MissingAccess-Control-Allow-Originbecomes opaqueTypeError: Failed to fetchinTermsGate.Observed production behavior (pre-fix)
OriginAccess-Control-Allow-Originechoed?https://terms.cl8y.comhttps://dex.cl8y.comAPI is reachable; CORS deny is what breaks the DEX gate.
Related (out of scope for this issue)
VITE_REDIRECT_URI_ALLOWLIST(web/src/redirect.ts+ SDKsanitizeRedirectUri). That is a separate allowlist; do not fold redirect policy into this CORS change unless explicitly expanded later.POST /admin/properties,scripts/register-property.sh) does not grant CORS.Why this is needed
dex.cl8y.com, future subdomains). Listing every origin in deploy env is brittle and causes silent browser failures when a new host is missed.Access-Control-Allow-Origin: *.cl8y.com. The server must match a pattern and echo the request Origin. That requires application logic (AllowOrigin::predicate), not a literal wildcard header.CORS_ORIGINS=*— Full open CORS is explicitly discouraged for production (see audits / README hygiene). Subdomain patterns are the least-privilege alternative for first-party hosts.Constraints & guardrails
Scheme-bound patterns only — Supported form:
https://*.example.com(and optionallyhttp://*.example.comfor local/dev). Bare*.example.comwithout scheme must be rejected or treated as config error (prefer reject at startup / ignore with clear log — pick one and document).HTTPS in production — Document that prod should use
https://*.cl8y.com, nothttp://….Safe host matching — Allow:
https://cl8y.comwhen pattern ishttps://*.cl8y.comhttps://dex.cl8y.com,https://a.b.cl8y.comReject:
https://cl8y.com.evil.com,https://evilcl8y.com,https://notcl8y.comhttp://dex.cl8y.comwhen onlyhttps://*.cl8y.comis configurednullOrigin via patternPorts — Pattern match default ports only unless an exact origin with port is also listed (e.g.
http://localhost:5173remains exact-only). Non-default ports onhttps://dex.cl8y.com:8443must not matchhttps://*.cl8y.comunless explicitly decided otherwise; default = deny non-default ports for patterns.Keep exact origins — Exact entries continue to work and can be mixed with patterns in one
CORS_ORIGINSvalue.Keep
CORS_ORIGINS=*— Existing full-open behavior remains for tests/dev; do not change its meaning.No credentials widening — Do not introduce
Access-Control-Allow-Credentials: trueas part of this work unless already required (current layer uses methods/headersAnywithout credentials cookies for the public API).Docs only for redirect — Call out that DEX still needs
https://dex.cl8y.comonVITE_REDIRECT_URI_ALLOWLISTfor return-after-sign; that is not solved by CORS patterns.Relevant files
api/src/routes/mod.rsbuild_cors— primary change (AllowOrigin::predicate)api/src/config.rsCORS_ORIGINSparsing (may validate pattern syntax)api/tests/integration_test.rscors_origins: vec!["*"]— add focused CORS cases.env.exampleREADME.mdCORS_ORIGINSpackages/cl8y-clickwrap/README.mdaudits/INTERNAL_COMPOSER_1786408744.mdCORS_ORIGINS=*in prodOptional helper module (if
mod.rsgrows): e.g.api/src/cors.rsfor parse + match unit tests.Recommended direction
Parse each
CORS_ORIGINSentry into:Exact(HeaderValue), orWildcard { scheme: https|http, base_host: "cl8y.com" }forhttps://*.cl8y.com/http://*.cl8y.comBuild
CorsLayerwithAllowOrigin::predicatethat returns true when the request Origin:url::Url/ origin and matches a wildcard rule (scheme, host apex-or-subdomain, default port)Rely on tower-http to mirror the allowed request Origin into
Access-Control-Allow-Origin(do not emit a literal*.host).Recommended production setting after deploy:
(
https://terms.cl8y.comis covered by the same pattern.)Add unit tests for the matcher; add at least one HTTP-level test that OPTIONS/GET from an allowed subdomain receives
access-control-allow-originechoing that origin, and a denied origin does not.Acceptance criteria
CORS_ORIGINS=https://*.cl8y.comallowsOrigin: https://dex.cl8y.comand echoes that origin on preflight and actual responses.Origin: https://cl8y.comand existing portalhttps://terms.cl8y.com.https://*.cl8y.com,https://partner.example).CORS_ORIGINS=*behavior unchanged (allow any).Access-Control-Allow-Origin..env.example+ README document pattern syntax, apex inclusion, and that redirect allowlist is separate.Test plan (functional paths)
Exact only (regression)
Config:
https://terms.cl8y.comOrigin: https://terms.cl8y.com→ ACAO echoedOrigin: https://dex.cl8y.com→ no ACAOWildcard only
Config:
https://*.cl8y.comhttps://dex.cl8y.com,https://terms.cl8y.com,https://cl8y.com,https://a.b.cl8y.comhttp://dex.cl8y.com,https://evil.com,https://cl8y.com.evil.com,https://evilcl8y.comMixed
Config:
https://*.cl8y.com,https://app.partner.exampleFull open
Config:
*Preflight + GET
OPTIONSandGET /api/v1/signatures/status?...both carry ACAO when Origin is allowedPort edge
https://dex.cl8y.com:8443denied underhttps://*.cl8y.com(unless exact entry added)http://127.0.0.1:5173still works for local CI/PlaywrightDocs smoke
.env.examplewithout reading sourceTest plan (attack / abuse / hack vectors)
https://cl8y.com.attacker.comhttps://evilcl8y.com/https://not-cl8y.comhttp://dex.cl8y.comwith https-only patternOrigin: nullhttps://*.cl8y.com.evil.com)cl8y.com*or*.cl8y.comvia misconfigAnypath*together with other entries["*"]enables Any; mixed*,https://…should not silently open all — lock this in tests)ADMIN_TOKENeven from allowed OriginVerification criteria
Unit / integration: Matcher and HTTP CORS tests green in CI for allow + deny matrices above.
Local manual: Run API with
CORS_ORIGINS=https://*.cl8y.comand:Production after deploy: Same OPTIONS/GET probes against
https://api.terms.cl8y.comwithOrigin: https://dex.cl8y.comshow echoed ACAO; loadinghttps://dex.cl8y.comno longer showsUnable to verify terms acceptance: Failed to fetchfor network/CORS reasons (subsequent product errors, if any, must be explicit API error strings).Redirect still checked separately: Signing return to DEX still requires portal allowlist entry — verify docs mention this so CORS fix is not mistaken for full end-to-end sign-return readiness.
Out of scope
VITE_REDIRECT_URI_ALLOWLISTwildcardsallow_methods/allow_headerspolicy beyond currentAnyCORS_ORIGINS=*to productionmentioned in issue #8
mentioned in issue #12
mentioned in issue #13
mentioned in issue #14