Skip to content

Fix nginx deny-all shadowing the /storage public-file allow-list - #228

Merged
morcen merged 1 commit into
mainfrom
fix/issue-139-nginx-storage-shadow
Sep 18, 2026
Merged

morcen merged 1 commit into
mainfrom
fix/issue-139-nginx-storage-shadow

Conversation

@morcen

@morcen morcen commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

What was broken

docker/nginx.conf had:

# Security: deny access to storage
location ^~ /storage {
    deny all;
}

# Allow specific storage paths for public files
location ~* /storage/.*\.(jpg|jpeg|png|gif|ico|svg|pdf)$ {
    expires 30d;
    add_header Cache-Control "public, no-transform";
}

The ^~ prefix modifier tells nginx to stop searching for a matching location and skip evaluating any regex locations once this prefix location is the longest match. Since /storage matched ^~ first, the regex allow-list right below it was never reached, so every request under /storage was denied with a 403 — including public files served from the public filesystem disk, such as Jetstream profile photos (profile_photo_disk = public in config/jetstream.php, served from APP_URL/storage).

What changed

  • Removed the ^~ modifier from the /storage deny-all location in docker/nginx.conf. Without it, nginx still evaluates the regex allow-list location for allowed extensions (jpg/jpeg/png/gif/ico/svg/pdf) before falling back to the deny-all rule for anything else under /storage.
  • Added tests/Feature/NginxStorageAccessTest.php, which parses the committed docker/nginx.conf (nginx doesn't run under the PHPUnit suite) and asserts the deny rule no longer uses ^~ while both the deny-all fallback and the extension allow-list still exist. This follows the same pattern as the existing NginxContentSecurityPolicyTest. Verified the new test fails against the pre-fix config and passes after the fix.

Test plan

  • vendor/bin/pint --dirty — clean
  • composer test (full PHPUnit suite) — 261 passed, 1 pre-existing skip
  • Verified tests/Feature/NginxStorageAccessTest.php fails on the original ^~ /storage config and passes after the fix

Fixes #139

…w-list

location ^~ /storage { deny all; } used the ^~ prefix modifier, which
tells nginx to stop searching and skip all regex locations once this
prefix matches. That silently shadowed the regex location right below
it that was meant to allow public image/PDF files under /storage, so
every request under /storage was denied with a 403 regardless of the
allow-list, including Jetstream profile photos served from the public
disk.

Dropping ^~ lets nginx fall through to the regex allow-list for
requests that match an allowed extension, while everything else still
hits the deny-all fallback.

Adds a regression test that parses the committed nginx.conf (nginx
doesn't run under the PHPUnit suite) and asserts the deny rule no
longer uses ^~, following the existing pattern used for the CSP header
test.
@morcen
morcen merged commit 3b2f12e into main Sep 18, 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.

nginx's location ^~ /storage { deny all; } shadows the allow-list regex below it — public storage assets always 403

1 participant