server { listen 80; server_name _; root /var/www/html/public; index index.php; client_max_body_size 100m; # The nginx version number is nobody's business but ours. server_tokens off; # Baseline hardening for every response. `always` so they survive error # responses too. NOTE: nginx does not merge add_header across levels — # a location that declares any add_header of its own inherits none of # these, so /protected-files/ below repeats them deliberately. add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; location / { try_files $uri $uri/ /index.php?$query_string; } # Protected file serving: PHP authorizes, nginx streams (brief §3). # PHP responds with X-Accel-Redirect: /protected-files/. # # This is the only location that returns bytes someone else uploaded, # so it gets the strictest headers in the file. `sandbox` puts anything # rendered as a document into an opaque origin with scripts disabled — # if a payload ever does reach here with a renderable content type # (FileThumbnailController's allowlist is the primary defence), it # cannot touch this app's origin or the viewer's session. Images loaded # as subresources are unaffected: a CSP on a subresource response never # creates a browsing context, so thumbnails and previews still render. # `^~` so this prefix beats the `\.php$` regex below: without it a # protected path ending in .php would be handed to the PHP handler # instead of streaming under the sandbox headers this block sets. location ^~ /protected-files/ { internal; alias /var/www/html/storage/app/files/; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "sandbox; default-src 'none'" always; } location ~ \.php$ { # Never hand a path to PHP-FPM that isn't a real script on disk: # without this, any URI ending in .php reaches the interpreter and # PATH_INFO resolution decides what actually runs. try_files $uri =404; fastcgi_pass app:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_buffer_size 32k; fastcgi_buffers 8 32k; } location ~ /\.(?!well-known) { deny all; } }