Skip to content

[992] CORS Configuration Validation and Dynamic Origin Allowlist - #304

Merged
salazarsebas merged 3 commits into
salazarsebas:mainfrom
ToryMic:fix/992-cors-configuration-validation-and-dynamic-origin-allowlist
Aug 30, 2026
Merged

[992] CORS Configuration Validation and Dynamic Origin Allowlist#304
salazarsebas merged 3 commits into
salazarsebas:mainfrom
ToryMic:fix/992-cors-configuration-validation-and-dynamic-origin-allowlist

Conversation

@ToryMic

@ToryMic ToryMic commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #299


Closes 992

Summary

Adds cougr_core::cors, a dependency-free, no_std-compatible CORS policy layer for HTTP gateways (indexers, session servers, leaderboard APIs) that sit in front of Cougr game contracts.

What's included

Configuration validation (CorsConfig::validate)

  • Origin syntax enforcement: http/https schemes only; rejects paths, queries, fragments, userinfo, whitespace, invalid hosts/labels, and invalid ports (non-numeric, 0, out of range, leading zeros)
  • Rejects unsafe combinations up front: allow_credentials = true together with a wildcard (*) origin, method, or header — the combination browsers refuse
  • Method and header-name tokens validated against RFC 7230 token grammar
  • Preflight max-age bounded by MAX_ALLOWED_MAX_AGE_SECONDS

Dynamic origin allowlist (OriginAllowlist)

  • Runtime-mutable: add / remove / clear take effect immediately on live policies
  • Three entry shapes: exact origins, single-label wildcard subdomains (https://*.example.com matches api.example.com but not a.b.example.com or the apex), and global *
  • Browser-grade matching semantics: case-insensitive scheme/host and default-port folding (http://host:80http://host)

Request evaluation

  • evaluate_preflight returns a PreflightDecision carrying ready-to-emit Access-Control-* header values (echoed origin vs. *, granted headers incl. safelisted ones) or the canonical denied decision
  • response_allow_origin mirrors the same rules for actual (non-preflight) responses

Testing

  • 20 in-module unit tests + 7 integration tests (tests/integration_cors.rs)
  • Full suite green locally: cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, cargo test --workspace --features testutils, cargo build, and cougr check hygiene pass

ToryMic added 2 commits August 24, 2026 19:48
Introduce cougr_core::cors, a no_std policy layer for HTTP gateways in
front of game contracts:

- CorsConfig validates origins, methods, header names, and max age,
  rejecting unsafe combinations such as credentials plus wildcard
  origin/method/header values
- OriginAllowlist is runtime-mutable with exact origins, single-label
  wildcard subdomain patterns (https://*.example.com), and a global '*'
  entry; matching normalizes scheme/host case and default ports
- evaluate_preflight returns ready-to-emit response headers or the
  canonical denied decision

Covered by in-module unit tests and tests/integration_cors.rs.
@ToryMic

ToryMic commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Note for maintainers: the Core CI and Cougr Site workflow runs for this fork PR are in action_required state and need a one-click approval from someone with write access before they can execute (runs, runs).

All CI steps were verified locally against the head commit (8151abe) with the exact commands from core.yml:

  • cargo fmt --check — clean
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo test --workspace --features testutils — all 26 test binaries/doc-test groups pass, 0 failures
  • cargo build — ok
  • cargo run -p cougr-cli -- check --path . — ALL CHECKS PASSED
  • python3 cougr-site/sync.py — committed so the site sync check stays clean

@salazarsebas salazarsebas left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Excellent test coverage and careful spec-following on origin parsing (default ports, IPv6, wildcard subdomains). One design gap that matters given what this module is for:

evaluate_preflight() and response_allow_origin() never call validate() internally. If a caller sets allow_credentials = true with a wildcard "*" origin and calls either method directly (e.g. after mutating the allowlist at runtime post-startup, without re-validating), origins.allows(origin) returns true for any arbitrary origin because AllowEntry::Any matches everything, and the response echoes that attacker-controlled origin back with Access-Control-Allow-Credentials: true. That's precisely the origin+credentials leak this crate exists to prevent, and every test in the suite avoids it by always calling .validate() first.

Given this is meant to be the safety layer other gateway code trusts, I'd rather not ship it with a footgun that quiet. Two options: have evaluate_preflight/response_allow_origin call self.validate() internally and deny-by-default on failure, or introduce a Validated<CorsConfig> wrapper that's the only type those methods accept, constructible only via validate(). Happy to merge once one of those lands.

@ToryMic ToryMic left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed

@salazarsebas

Copy link
Copy Markdown
Owner

Hey @ToryMic , what are the updates on this pull request?

evaluate_preflight and response_allow_origin now call validate() internally
and deny-by-default when the policy is unsafe, closing the credentials +
wildcard origin leak if callers skip validation or mutate the allowlist at runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ToryMic

ToryMic commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@salazarsebas I Addressed the review feedback in 17066d7 — went with internal re-validation rather than a Validated<CorsConfig> wrapper.

What changed

evaluate_preflight and response_allow_origin now call validate() on every invocation. If validation fails, they deny-by-default:

  • preflight → PreflightDecision::denied() (no CORS headers emitted)
  • actual response → None for Access-Control-Allow-Origin

This closes the footgun where allow_credentials = true + wildcard * origin could echo an attacker-controlled origin back with credentials enabled when callers skipped .validate() or mutated the allowlist at runtime after startup.

Tests added (3 unit + 2 integration) covering:

  • unsafe policy used without prior .validate()
  • valid policy that becomes invalid after runtime mutation (credentials + wildcard origin, credentials + wildcard method)

Verified locally: cargo fmt --check, unit + integration CORS tests (32 total) all green.

@salazarsebas salazarsebas left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-validation fix confirmed — evaluate_preflight() and response_allow_origin() now call validate() and deny by default on failure, closing the credentials+wildcard gap. CI green on 17066d7. Approving.

@salazarsebas
salazarsebas merged commit 27ccb3e into salazarsebas:main Aug 30, 2026
2 checks passed
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.

docs(testing): write a standalone GameHarness/Scenario/SnapshotAssert testing guide

2 participants