Skip to content

fix(security): fail-closed CORS default for prod image (#319)#330

Open
dmytrocraft wants to merge 3 commits into
mainfrom
security/319-prod-cors-dev-regex-credentials
Open

fix(security): fail-closed CORS default for prod image (#319)#330
dmytrocraft wants to merge 3 commits into
mainfrom
security/319-prod-cors-dev-regex-credentials

Conversation

@dmytrocraft

@dmytrocraft dmytrocraft commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Security fix — Closes #319

Vulnerability (CWE-942, HIGH)

The committed .env shipped a working dev CORS allow-list:

CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'

config/packages/nelmio_cors.yaml enables origin_regex: true and allow_credentials: true for all paths (/api, /api/graphql, /authorize) and reads allow_origin from %env(CORS_ALLOW_ORIGIN)%. The Dockerfile prod stage runs composer dump-env prod, which bakes that localhost regex into the production image (.env.local.php). Unless an operator explicitly set CORS_ALLOW_ORIGIN at deploy time, the production API reflected any http(s)://localhost / 127.0.0.1 Origin into Access-Control-Allow-Origin together with Access-Control-Allow-Credentials: true. Because the JWT auth token is accepted from the __Host-auth_token cookie and the Authorization header, a browser context the browser treats as Origin: http://localhost (local dev server, webview, browser-extension page, DNS-rebinding to 127.0.0.1) could read authenticated cross-origin responses.

An empty value is not a safe fallback: nelmio compiles allow_origin: [''] to the regex {}i, which matches every origin (fail-open, verified). So the fix uses a non-empty deny-all default.

Fix

  • .env: default CORS_ALLOW_ORIGIN to the deny-all regex '(?!)' (matches no origin) and document it as a required HTTPS-only deploy variable. The dev localhost regex no longer ships in the prod artifact — prod now fails closed.
  • .env.dev: add the localhost allow-list for local development.
  • .env.test: keep the localhost allow-list (documented as a test override).
  • tests/Unit/Config/CorsAllowOriginDefaultTest.php: new regression test that pins the fail-closed default, asserts it denies localhost/127.0.0.1/prod/evil origins, asserts it is non-empty (not fail-open), confirms dev/test keep their localhost allow-lists, and confirms nelmio still binds allow_origin to %env(CORS_ALLOW_ORIGIN)% in dev/test/prod.

Local verification (one-off containers, vendor mounted read-only)

  • phpunit Unit --filter Cors: OK (5 tests, 25 assertions) — and it fails against the original .env (proving it is a real regression test).
  • phpunit full Unit suite: OK (2263 tests) — no regressions.
  • deptrac: Violations 0, Errors 0.
  • psalm (changed file): No errors found.
  • php-cs-fixer (changed file): 0 of 1 files.

BMAD spec

specs/security-319-prod-cors-dev-regex-credentials/ (prd.md, stories.md).

🤖 Generated with Claude Code


Summary by cubic

Sets a fail-closed CORS default for the production image to prevent localhost origins with credentials from being accepted when CORS_ALLOW_ORIGIN isn’t set (CWE-942). Moves the localhost regex to dev/test and adds a regression test to lock this down.

  • Bug Fixes

    • .env: default CORS_ALLOW_ORIGIN to (?!) (deny-all) and document it as a required deploy variable.
    • .env.dev and .env.test: use the localhost allow-list for local and CI.
    • Tests: add CorsAllowOriginDefaultTest to pin the deny-all default and verify allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] across dev/test/prod in nelmio_cors.yaml (via nelmio/cors-bundle).
    • CI: adjust phpinsights/Infection/test expectations so pipelines pass with the new defaults.
    • Specs: make verification commands portable using $APP_ROOT and $APP_IMAGE in specs/security-319-prod-cors-dev-regex-credentials/stories.md.
  • Migration

    • In production, set CORS_ALLOW_ORIGIN to your trusted HTTPS origins (regex), e.g. '^https://(app|www).example.com$'. An empty value is unsafe under nelmio/cors-bundle; until set, cross-origin requests are denied.

Written for commit 7602d60. Summary will update on new commits.

Review in cubic

The committed .env shipped a working dev CORS allow-list
(CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$').
nelmio_cors enables origin_regex + allow_credentials for all paths, and
the Dockerfile prod stage runs `composer dump-env prod`, baking that
localhost regex into the production image. Unless an operator set
CORS_ALLOW_ORIGIN at deploy time, the prod API reflected any
http(s)://localhost / 127.0.0.1 Origin into Access-Control-Allow-Origin
with Access-Control-Allow-Credentials: true (CWE-942), letting a
localhost browser context read authenticated cross-origin responses.

An empty value is not a safe fallback: nelmio compiles allow_origin:['']
to the regex {}i which matches EVERY origin (fail-open). The fix uses a
non-empty deny-all default instead.

Fix:
- .env: default CORS_ALLOW_ORIGIN to the deny-all regex '(?!)' (matches no
  origin) and document it as a required HTTPS-only deploy variable. The
  dev localhost regex no longer ships in the prod artifact.
- .env.dev: add the localhost allow-list for local development.
- .env.test: keep the localhost allow-list (documented as a test override).
- tests/Unit/Config/CorsAllowOriginDefaultTest.php: pin the fail-closed
  default; assert it denies localhost/127.0.0.1/prod/evil origins, is not
  empty (fail-open), keeps dev/test localhost allow-lists, and that nelmio
  still binds allow_origin to %env(CORS_ALLOW_ORIGIN)% in all envs.

Local verification (one-off containers, vendor mounted read-only):
- phpunit Unit --filter Cors: OK (5 tests, 25 assertions); fails on
  original .env (proves real regression test)
- phpunit full Unit suite: OK (2263 tests)
- deptrac: Violations 0, Errors 0
- psalm (changed file): No errors found
- php-cs-fixer (changed file): 0 of 1 files

Closes #319

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@dmytrocraft, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 6 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ea9cd6b3-7572-4836-8ac1-3eb80fcd6911

📥 Commits

Reviewing files that changed from the base of the PR and between fe53ccb and 7602d60.

📒 Files selected for processing (6)
  • .env
  • .env.dev
  • .env.test
  • specs/security-319-prod-cors-dev-regex-credentials/prd.md
  • specs/security-319-prod-cors-dev-regex-credentials/stories.md
  • tests/Unit/Config/CorsAllowOriginDefaultTest.php
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/319-prod-cors-dev-regex-credentials

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files

Confidence score: 5/5

  • This looks low risk to merge: the only finding is low severity (3/10) and affects contributor workflow rather than runtime behavior.
  • In specs/security-319-prod-cors-dev-regex-credentials/stories.md, verification commands use machine-specific absolute paths (/home/kravtsov/Projects/...), so other developers will need to edit commands before running them.
  • Pay close attention to specs/security-319-prod-cors-dev-regex-credentials/stories.md - replace hardcoded absolute paths with portable/project-relative commands.
Architecture diagram
sequenceDiagram
    participant ENV as .env (Committed)
    participant Dotenv as Symfony Dotenv
    participant CI as Docker Build (prod)
    participant Runtime as PHP Runtime (prod)
    participant CORS as nelmio/cors-bundle
    participant Browser as Browser (client)

    Note over ENV,Browser: CORS Configuration Resolution Flow

    ENV->>Dotenv: Contains CORS_ALLOW_ORIGIN='(?!)'
    Dotenv->>CI: composer dump-env prod bakes into .env.local.php
    CI->>Runtime: Production image ships baked .env.local.php
    Runtime->>CORS: Reads %env(CORS_ALLOW_ORIGIN)%
    CORS->>CORS: Compiles '(?!)' as regex {(?!)}i

    alt Dev/Test environment (.env.dev / .env.test)
        ENV->>Dotenv: Contains CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
        Dotenv->>Runtime: Overrides .env default
        Runtime->>CORS: Reads %env(CORS_ALLOW_ORIGIN)% = localhost regex
        Browser->>CORS: OPTIONS /api with Origin: http://localhost:3000
        CORS->>CORS: Regex matches localhost
        CORS-->>Browser: Access-Control-Allow-Origin: http://localhost:3000 + Access-Control-Allow-Credentials: true
    else Production (no operator override)
        Runtime->>CORS: Uses deny-all regex from baked .env
        Browser->>CORS: OPTIONS /api with Origin: http://localhost
        CORS->>CORS: Regex {(?!)}i matches NO origin
        CORS-->>Browser: No CORS headers (fail-closed)
    else Production (operator configured)
        ENV->>Runtime: Operator sets CORS_ALLOW_ORIGIN='^https://(app|www)\.example\.com$'
        Browser->>CORS: OPTIONS /api with Origin: https://app.example.com
        CORS->>CORS: Regex matches trusted origin
        CORS-->>Browser: Access-Control-Allow-Origin: https://app.example.com + Access-Control-Allow-Credentials: true
    end

    Note over ENV,CORS: Key Security Boundary: Production image never ships dev regex
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread specs/security-319-prod-cors-dev-regex-credentials/stories.md Outdated
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (fe53ccb) to head (7602d60).

Additional details and impacted files
@@              Coverage Diff              @@
##                main      #330     +/-   ##
=============================================
  Coverage     100.00%   100.00%             
- Complexity         0      2768   +2768     
=============================================
  Files            551       551             
  Lines           9541      9541             
=============================================
  Hits            9541      9541             
Flag Coverage Δ
unittests 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

Replace hardcoded developer-machine absolute host paths in the spec
verification commands with portable $APP_ROOT (git rev-parse) and a
configurable $APP_IMAGE so any contributor can run them as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dmytrocraft

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Re-trigger cubic

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.

[Security] high: Production image ships dev-only CORS origin regex with Access-Control-Allow-Credentials: true

1 participant