Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
SecurityHeaders
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
2 / 2
2
100.00% covered (success)
100.00%
1 / 1
 handle
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 csp
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Middleware;
4
5use Closure;
6use Illuminate\Http\Request;
7use 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 */
26class 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}