From d812be4353b735b15c1be6cae8dc6d94c94730f1 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 27 Mar 2025 15:34:50 +0100 Subject: [PATCH 1/3] feat: ajout du middleware CSP --- composer.json | 1 + .../system/framework/Middlewares/Csp.spec.php | 117 ++++++++++++++++++ src/Middlewares/Csp.php | 86 +++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 spec/system/framework/Middlewares/Csp.spec.php create mode 100644 src/Middlewares/Csp.php diff --git a/composer.json b/composer.json index f719196a..551869ef 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "filp/whoops": "^2.15", "kahlan/kahlan": "^5.2", "mikey179/vfsstream": "^1.6", + "paragonie/csp-builder": "^3.0", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^1.11", "phpstan/phpstan-strict-rules": "^1.6", diff --git a/spec/system/framework/Middlewares/Csp.spec.php b/spec/system/framework/Middlewares/Csp.spec.php new file mode 100644 index 00000000..d41914a4 --- /dev/null +++ b/spec/system/framework/Middlewares/Csp.spec.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use BlitzPHP\Http\Response; +use BlitzPHP\Http\ServerRequestFactory; +use BlitzPHP\Middlewares\Csp; +use ParagonIE\CSPBuilder\CSPBuilder; +use Spec\BlitzPHP\Middlewares\TestRequestHandler; + +use function Kahlan\expect; + +describe('Middleware / Csp', function (): void { + beforeAll(function () { + $this->getRequestHandler = function () { + return new TestRequestHandler(function ($request) { + return new Response(); + }); + }; + }); + + it('Process ajoute les headers', function (): void { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/test']); + + $middleware = new Csp([ + 'script-src' => [ + 'allow' => [ + 'https://www.google-analytics.com', + ], + 'self' => true, + 'unsafe-inline' => false, + 'unsafe-eval' => false, + ], + ]); + + $response = $middleware->process($request, $this->getRequestHandler()); + $policy = $response->getHeaderLine('Content-Security-Policy'); + + $expected = "script-src 'self' https://www.google-analytics.com"; + + expect(str_contains($policy, $expected))->toBeTruthy(); + expect(str_contains($policy, 'nonce-'))->toBeFalsy(); + }); + + it('Process ajoute les attributs de requete pour nonces', function (): void { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/test']); + + $policy = [ + 'script-src' => [ + 'self' => true, + 'unsafe-inline' => false, + 'unsafe-eval' => false, + ], + 'style-src' => [ + 'self' => true, + 'unsafe-inline' => false, + 'unsafe-eval' => false, + ], + ]; + + $middleware = new Csp($policy, [ + 'script_nonce' => true, + 'style_nonce' => true, + ]); + + $handler = new TestRequestHandler(function ($request) { + expect($request->getAttribute('cspScriptNonce'))->not->toBeEmpty(); + expect($request->getAttribute('cspStyleNonce'))->not->toBeEmpty(); + + return new Response(); + }); + + $response = $middleware->process($request, $handler); + $policy = $response->getHeaderLine('Content-Security-Policy'); + $expected = [ + "script-src 'self' 'nonce-", + "style-src 'self' 'nonce-", + ]; + + expect($policy)->not->toBeEmpty(); + + foreach ($expected as $match) { + expect(str_contains($policy, $match))->toBeTruthy(); + } + }); + + it('Passage d\'une instance CSPBuilder', function () { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/test']); + + $config = [ + 'script-src' => [ + 'allow' => [ + 'https://www.google-analytics.com', + ], + 'self' => true, + 'unsafe-inline' => false, + 'unsafe-eval' => false, + ], + ]; + + $cspBuilder = new CSPBuilder($config); + $middleware = new Csp($cspBuilder); + + $response = $middleware->process($request, $this->getRequestHandler()); + $policy = $response->getHeaderLine('Content-Security-Policy'); + $expected = "script-src 'self' https://www.google-analytics.com"; + + expect(str_contains($policy, $expected))->toBeTruthy(); + }); +}); diff --git a/src/Middlewares/Csp.php b/src/Middlewares/Csp.php new file mode 100644 index 00000000..b1e160b8 --- /dev/null +++ b/src/Middlewares/Csp.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Middlewares; + +use BlitzPHP\Exceptions\FrameworkException; +use BlitzPHP\Traits\InstanceConfigTrait; +use ParagonIE\CSPBuilder\CSPBuilder; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * Content Security Policy Middleware + * + * ### Options + * + * - `script_nonce` Permet d'ajouter une politique de nonce à la directive script-src. + * - `style_nonce` Permet d'ajouter une politique de nonce à la directive style-src. + */ +class Csp implements MiddlewareInterface +{ + use InstanceConfigTrait; + + /** + * CSP Builder + */ + protected CSPBuilder $csp; + + /** + * Options de configuration. + * + * @var array + */ + protected array $_defaultConfig = [ + 'script_nonce' => false, + 'style_nonce' => false, + ]; + + /** + * Constructor + * + * @param CSPBuilder|array $csp Objet CSP ou tableau de configuration + * @param array $config options de configurations. + */ + public function __construct(CSPBuilder|array $csp, array $config = []) + { + if (!class_exists(CSPBuilder::class)) { + throw new FrameworkException('Vous devez installer paragonie/csp-builder pour utiliser le middleware Csp.'); + } + + $this->setConfig($config); + + if (!$csp instanceof CSPBuilder) { + $csp = new CSPBuilder($csp); + } + + $this->csp = $csp; + } + + /** + * Ajoute les nonces (s'ils sont activés) à la requete et applique l'en-tête CSP à la réponse. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($this->getConfig('script_nonce')) { + $request = $request->withAttribute('cspScriptNonce', $this->csp->nonce('script-src')); + } + if ($this->getconfig('style_nonce')) { + $request = $request->withAttribute('cspStyleNonce', $this->csp->nonce('style-src')); + } + + $response = $handler->handle($request); + + return $this->csp->injectCSPHeader($response); + } +} From 0ed105f5927b3b6965c3525c60e0e84e5054cae6 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 27 Mar 2025 15:38:56 +0100 Subject: [PATCH 2/3] cs fix --- src/Middlewares/Csp.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Middlewares/Csp.php b/src/Middlewares/Csp.php index b1e160b8..7d94a2b0 100644 --- a/src/Middlewares/Csp.php +++ b/src/Middlewares/Csp.php @@ -49,18 +49,18 @@ class Csp implements MiddlewareInterface /** * Constructor * - * @param CSPBuilder|array $csp Objet CSP ou tableau de configuration + * @param array|CSPBuilder $csp Objet CSP ou tableau de configuration * @param array $config options de configurations. */ - public function __construct(CSPBuilder|array $csp, array $config = []) + public function __construct(array|CSPBuilder $csp, array $config = []) { - if (!class_exists(CSPBuilder::class)) { + if (! class_exists(CSPBuilder::class)) { throw new FrameworkException('Vous devez installer paragonie/csp-builder pour utiliser le middleware Csp.'); } $this->setConfig($config); - if (!$csp instanceof CSPBuilder) { + if (! $csp instanceof CSPBuilder) { $csp = new CSPBuilder($csp); } From 1537fb9ce4b85193ffe4fef3fdc2e7e4234182f9 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Thu, 27 Mar 2025 15:41:35 +0100 Subject: [PATCH 3/3] fix phpstan --- src/Middlewares/Csp.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Middlewares/Csp.php b/src/Middlewares/Csp.php index 7d94a2b0..c6bb8ee1 100644 --- a/src/Middlewares/Csp.php +++ b/src/Middlewares/Csp.php @@ -75,12 +75,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($this->getConfig('script_nonce')) { $request = $request->withAttribute('cspScriptNonce', $this->csp->nonce('script-src')); } - if ($this->getconfig('style_nonce')) { + if ($this->getConfig('style_nonce')) { $request = $request->withAttribute('cspStyleNonce', $this->csp->nonce('style-src')); } $response = $handler->handle($request); + /** @var ResponseInterface */ return $this->csp->injectCSPHeader($response); } }