nosniff makes the browser obey Content-Type (MIME Types), Permissions-Policy turns off unused APIs, COOP cuts ties to cross-origin popups (same-origin-allow-popups keeps OAuth and payment windows working), and CSP limits where scripts, images and frames may come from.
# /var/www/example/.htaccess: security headers for the site, CORS for /api/
# Needs: mod_headers, mod_setenvif, mod_rewrite; AllowOverride FileInfo; Options FollowSymLinks
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data:; \
object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'"
# CORS: only these two origins may read /api/ from a browser, with cookies
SetEnvIf Origin "^https://(app|admin)\.example\.com$" CORS_OK=$0
<If "%{REQUEST_URI} =~ m#^/api/#">
Header always set Access-Control-Allow-Origin "%{CORS_OK}e" env=CORS_OK
Header always set Access-Control-Allow-Credentials "true" env=CORS_OK
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE" env=CORS_OK
Header always set Access-Control-Allow-Headers "Content-Type, Authorization" env=CORS_OK
Header always set Access-Control-Max-Age "7200" env=CORS_OK
Header always merge Vary "Origin"
</If>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} =OPTIONS
RewriteCond %{ENV:CORS_OK} .
RewriteRule ^api/ - [R=204,L]A preflight from an allowed origin, then a GET from another (the page itself received lines 3 to 9 verbatim):
HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://admin.example.com Access-Control-Allow-Credentials: true Access-Control-Max-Age: 7200 Vary: Origin HTTP/1.1 200 OK Vary: Origin
Line 7 covers browsers without CSP's frame-ancestors. Skip X-XSS-Protection: it is deprecated and could itself open XSS holes. Ship CSP first as Content-Security-Policy-Report-Only, since default-src 'self' blocks inline scripts. Credentials forbid Allow-Origin: *, so line 11 captures an allowed Origin and line 13 echoes it; line 18 stops a cache serving one origin's answer to another. Browsers cap Max-Age (Chromium 4,389 at 7,200 seconds, Firefox 555 at 86,400). Lines 19 to 22 answer the preflight with 204 before PHP runs. The stranger got no CORS headers, so a browser hides the body from it.