Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
SecurityHeadersMiddleware
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
2 / 2
3
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Security\Middleware;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Security\Csp\CspNonceManager;
12use App\Core\Security\Csp\CspNonceManagerInterface;
13use Psr\Http\Message\ResponseInterface;
14use Psr\Http\Message\ServerRequestInterface;
15use Psr\Http\Server\MiddlewareInterface;
16use Psr\Http\Server\RequestHandlerInterface;
17
18/**
19 * Security Headers Middleware.
20 *
21 * Enforces essential HTTP security response headers including CSP Level 3 with nonces, HSTS,
22 * X-Frame-Options, X-Content-Type-Options, and Referrer-Policy.
23 *
24 * @package App\Core\Security\Middleware
25 */
26final readonly class SecurityHeadersMiddleware implements MiddlewareInterface
27{
28    private CspNonceManagerInterface $nonceManager;
29
30    /**
31     * SecurityHeadersMiddleware constructor.
32     *
33     * @param bool                          $reportCsp    Whether to append CSP violation reporting endpoint.
34     * @param CspNonceManagerInterface|null $nonceManager Optional CSP nonce manager instance.
35     */
36    public function __construct(
37        private bool $reportCsp = true,
38        ?CspNonceManagerInterface $nonceManager = null
39    ) {
40        $this->nonceManager = $nonceManager ?? new CspNonceManager();
41    }
42
43    /**
44     * Process an incoming server request and append security headers to response.
45     *
46     * @param ServerRequestInterface  $request Server request.
47     * @param RequestHandlerInterface $handler Request handler.
48     *
49     * @return ResponseInterface Response enriched with security headers.
50     */
51    public function process(
52        ServerRequestInterface $request,
53        RequestHandlerInterface $handler
54    ): ResponseInterface {
55        $nonce = $this->nonceManager->getNonce();
56        $enrichedRequest = $request->withAttribute('csp_nonce', $nonce);
57        $response = $handler->handle($enrichedRequest);
58
59        $cspDirectives = [
60            "default-src 'self'",
61            "script-src 'self' 'nonce-{$nonce}'",
62            "style-src 'self' 'unsafe-inline'",
63            "font-src 'self' data:",
64            "img-src 'self' data: https:",
65            "connect-src 'self'",
66            "frame-ancestors 'self'",
67            "base-uri 'self'",
68            "form-action 'self'",
69            "upgrade-insecure-requests",
70        ];
71
72        if ($this->reportCsp) {
73            $cspDirectives[] = "report-uri /api/v1/logs/csp-violation";
74        }
75
76        $permissionsPolicy = [
77            'accelerometer=()',
78            'autoplay=()',
79            'camera=()',
80            'cross-origin-isolated=()',
81            'display-capture=()',
82            'encrypted-media=()',
83            'fullscreen=(self)',
84            'geolocation=()',
85            'gyroscope=()',
86            'magnetometer=()',
87            'microphone=()',
88            'midi=()',
89            'payment=()',
90            'picture-in-picture=()',
91            'publickey-credentials-get=()',
92            'screen-wake-lock=()',
93            'sync-xhr=()',
94            'usb=()',
95            'xr-spatial-tracking=()',
96        ];
97
98        return $response
99            ->withHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')
100            ->withHeader('X-Content-Type-Options', 'nosniff')
101            ->withHeader('X-Frame-Options', 'SAMEORIGIN')
102            ->withHeader('X-XSS-Protection', '0')
103            ->withHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
104            ->withHeader('Cross-Origin-Opener-Policy', 'same-origin')
105            ->withHeader('Cross-Origin-Resource-Policy', 'same-origin')
106            ->withHeader('Permissions-Policy', implode(', ', $permissionsPolicy))
107            ->withHeader('Content-Security-Policy', implode('; ', $cspDirectives));
108    }
109}