Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
23 / 23 |
|
100.00% |
2 / 2 |
CRAP | |
100.00% |
1 / 1 |
| SecurityHeaders | |
100.00% |
23 / 23 |
|
100.00% |
2 / 2 |
2 | |
100.00% |
1 / 1 |
| handle | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| csp | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Middleware; |
| 4 | |
| 5 | use Closure; |
| 6 | use Illuminate\Http\Request; |
| 7 | use Symfony\Component\HttpFoundation\Response; |
| 8 | |
| 9 | /** |
| 10 | * Phase 11 hardening — headers every response should carry, admin and |
| 11 | * storefront alike. The CSP is deliberately permissive on script-src/ |
| 12 | * style-src ('unsafe-inline') rather than a strict nonce-based policy: this |
| 13 | * app's admin theme and a lot of its own views rely on inline <script>/ |
| 14 | * <style> blocks throughout, and rewriting every one of them to use a nonce |
| 15 | * is a much larger, separate refactor. What this CSP still buys for free, |
| 16 | * with zero risk of breaking anything: no plugins/objects (object-src), |
| 17 | * no framing by another site (frame-ancestors — the real clickjacking |
| 18 | * defense), forms can only submit within the app (form-action), and a |
| 19 | * locked-down base-uri. |
| 20 | * |
| 21 | * https://dreamspos.dreamguystech.com is allow-listed here only because the |
| 22 | * admin theme currently loads several scripts straight from that live demo |
| 23 | * server — a reliability/supply-chain risk in its own right, tracked |
| 24 | * separately (self-hosting those files removes the need for this entry). |
| 25 | */ |
| 26 | class SecurityHeaders |
| 27 | { |
| 28 | public function handle(Request $request, Closure $next): Response |
| 29 | { |
| 30 | $response = $next($request); |
| 31 | |
| 32 | $response->headers->set('X-Content-Type-Options', 'nosniff'); |
| 33 | $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); |
| 34 | $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); |
| 35 | $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); |
| 36 | $response->headers->set('Content-Security-Policy', $this->csp()); |
| 37 | |
| 38 | return $response; |
| 39 | } |
| 40 | |
| 41 | private function csp(): string |
| 42 | { |
| 43 | // Every external domain here is a confirmed, currently-active integration |
| 44 | // (grepped the views, not guessed): GA4/GTM (Phase 6 <x-seo>), Meta Pixel |
| 45 | // and TikTok Pixel (both settings-driven, website/layouts/app.blade.php), |
| 46 | // plus the CDN/library domains the admin and storefront actually load |
| 47 | // scripts/styles/fonts from. |
| 48 | $scriptSrc = "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://code.jquery.com https://www.googletagmanager.com https://connect.facebook.net https://analytics.tiktok.com https://dreamspos.dreamguystech.com"; |
| 49 | $styleSrc = "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.bunny.net https://maxcdn.bootstrapcdn.com https://dreamspos.dreamguystech.com"; |
| 50 | $fontSrc = "'self' data: https://fonts.gstatic.com https://fonts.bunny.net https://cdnjs.cloudflare.com https://maxcdn.bootstrapcdn.com"; |
| 51 | |
| 52 | return implode('; ', [ |
| 53 | "default-src 'self'", |
| 54 | "script-src {$scriptSrc}", |
| 55 | "style-src {$styleSrc}", |
| 56 | "font-src {$fontSrc}", |
| 57 | // Images: allowed broadly (any http/https origin). Product/media |
| 58 | // images have turned up stored with absolute production-domain |
| 59 | // URLs rather than relative paths — a pre-existing data quirk, |
| 60 | // not something to silently break here — and img-src has little |
| 61 | // security value to begin with (it can't stop XSS or exfiltrate |
| 62 | // more than a URL's worth of data). |
| 63 | "img-src 'self' data: http: https:", |
| 64 | // Connect (fetch/XHR/beacon): allowed to any HTTPS endpoint rather |
| 65 | // than an enumerated allowlist. Meta Pixel's server-side Conversions |
| 66 | // API alone was observed beaconing to two different, seemingly |
| 67 | // account-specific gateway domains in testing — trying to allowlist |
| 68 | // every analytics vendor's dynamic beacon target is a losing game |
| 69 | // that just breaks marketing tooling unpredictably. Non-HTTPS |
| 70 | // exfiltration is still blocked, which is what actually matters here. |
| 71 | "connect-src 'self' https:", |
| 72 | "frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://www.facebook.com https://www.google.com", |
| 73 | "object-src 'none'", |
| 74 | "base-uri 'self'", |
| 75 | "form-action 'self'", |
| 76 | "frame-ancestors 'self'", |
| 77 | ]); |
| 78 | } |
| 79 | } |