Skip to content

Commit 658636b

Browse files
authored
Merge pull request #69 from blitz-php/devs
feat: ajout du middleware CSP
2 parents 2384357 + 1537fb9 commit 658636b

3 files changed

Lines changed: 205 additions & 0 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"filp/whoops": "^2.15",
4545
"kahlan/kahlan": "^5.2",
4646
"mikey179/vfsstream": "^1.6",
47+
"paragonie/csp-builder": "^3.0",
4748
"phpstan/extension-installer": "^1.4",
4849
"phpstan/phpstan": "^1.11",
4950
"phpstan/phpstan-strict-rules": "^1.6",
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Blitz PHP framework.
5+
*
6+
* (c) 2022 Dimitri Sitchet Tomkeu <devcode.dst@gmail.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
use BlitzPHP\Http\Response;
13+
use BlitzPHP\Http\ServerRequestFactory;
14+
use BlitzPHP\Middlewares\Csp;
15+
use ParagonIE\CSPBuilder\CSPBuilder;
16+
use Spec\BlitzPHP\Middlewares\TestRequestHandler;
17+
18+
use function Kahlan\expect;
19+
20+
describe('Middleware / Csp', function (): void {
21+
beforeAll(function () {
22+
$this->getRequestHandler = function () {
23+
return new TestRequestHandler(function ($request) {
24+
return new Response();
25+
});
26+
};
27+
});
28+
29+
it('Process ajoute les headers', function (): void {
30+
$request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/test']);
31+
32+
$middleware = new Csp([
33+
'script-src' => [
34+
'allow' => [
35+
'https://www.google-analytics.com',
36+
],
37+
'self' => true,
38+
'unsafe-inline' => false,
39+
'unsafe-eval' => false,
40+
],
41+
]);
42+
43+
$response = $middleware->process($request, $this->getRequestHandler());
44+
$policy = $response->getHeaderLine('Content-Security-Policy');
45+
46+
$expected = "script-src 'self' https://www.google-analytics.com";
47+
48+
expect(str_contains($policy, $expected))->toBeTruthy();
49+
expect(str_contains($policy, 'nonce-'))->toBeFalsy();
50+
});
51+
52+
it('Process ajoute les attributs de requete pour nonces', function (): void {
53+
$request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/test']);
54+
55+
$policy = [
56+
'script-src' => [
57+
'self' => true,
58+
'unsafe-inline' => false,
59+
'unsafe-eval' => false,
60+
],
61+
'style-src' => [
62+
'self' => true,
63+
'unsafe-inline' => false,
64+
'unsafe-eval' => false,
65+
],
66+
];
67+
68+
$middleware = new Csp($policy, [
69+
'script_nonce' => true,
70+
'style_nonce' => true,
71+
]);
72+
73+
$handler = new TestRequestHandler(function ($request) {
74+
expect($request->getAttribute('cspScriptNonce'))->not->toBeEmpty();
75+
expect($request->getAttribute('cspStyleNonce'))->not->toBeEmpty();
76+
77+
return new Response();
78+
});
79+
80+
$response = $middleware->process($request, $handler);
81+
$policy = $response->getHeaderLine('Content-Security-Policy');
82+
$expected = [
83+
"script-src 'self' 'nonce-",
84+
"style-src 'self' 'nonce-",
85+
];
86+
87+
expect($policy)->not->toBeEmpty();
88+
89+
foreach ($expected as $match) {
90+
expect(str_contains($policy, $match))->toBeTruthy();
91+
}
92+
});
93+
94+
it('Passage d\'une instance CSPBuilder', function () {
95+
$request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/test']);
96+
97+
$config = [
98+
'script-src' => [
99+
'allow' => [
100+
'https://www.google-analytics.com',
101+
],
102+
'self' => true,
103+
'unsafe-inline' => false,
104+
'unsafe-eval' => false,
105+
],
106+
];
107+
108+
$cspBuilder = new CSPBuilder($config);
109+
$middleware = new Csp($cspBuilder);
110+
111+
$response = $middleware->process($request, $this->getRequestHandler());
112+
$policy = $response->getHeaderLine('Content-Security-Policy');
113+
$expected = "script-src 'self' https://www.google-analytics.com";
114+
115+
expect(str_contains($policy, $expected))->toBeTruthy();
116+
});
117+
});

src/Middlewares/Csp.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Blitz PHP framework.
5+
*
6+
* (c) 2022 Dimitri Sitchet Tomkeu <devcode.dst@gmail.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace BlitzPHP\Middlewares;
13+
14+
use BlitzPHP\Exceptions\FrameworkException;
15+
use BlitzPHP\Traits\InstanceConfigTrait;
16+
use ParagonIE\CSPBuilder\CSPBuilder;
17+
use Psr\Http\Message\ResponseInterface;
18+
use Psr\Http\Message\ServerRequestInterface;
19+
use Psr\Http\Server\MiddlewareInterface;
20+
use Psr\Http\Server\RequestHandlerInterface;
21+
22+
/**
23+
* Content Security Policy Middleware
24+
*
25+
* ### Options
26+
*
27+
* - `script_nonce` Permet d'ajouter une politique de nonce à la directive script-src.
28+
* - `style_nonce` Permet d'ajouter une politique de nonce à la directive style-src.
29+
*/
30+
class Csp implements MiddlewareInterface
31+
{
32+
use InstanceConfigTrait;
33+
34+
/**
35+
* CSP Builder
36+
*/
37+
protected CSPBuilder $csp;
38+
39+
/**
40+
* Options de configuration.
41+
*
42+
* @var array<string, mixed>
43+
*/
44+
protected array $_defaultConfig = [
45+
'script_nonce' => false,
46+
'style_nonce' => false,
47+
];
48+
49+
/**
50+
* Constructor
51+
*
52+
* @param array|CSPBuilder $csp Objet CSP ou tableau de configuration
53+
* @param array<string, mixed> $config options de configurations.
54+
*/
55+
public function __construct(array|CSPBuilder $csp, array $config = [])
56+
{
57+
if (! class_exists(CSPBuilder::class)) {
58+
throw new FrameworkException('Vous devez installer paragonie/csp-builder pour utiliser le middleware Csp.');
59+
}
60+
61+
$this->setConfig($config);
62+
63+
if (! $csp instanceof CSPBuilder) {
64+
$csp = new CSPBuilder($csp);
65+
}
66+
67+
$this->csp = $csp;
68+
}
69+
70+
/**
71+
* Ajoute les nonces (s'ils sont activés) à la requete et applique l'en-tête CSP à la réponse.
72+
*/
73+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
74+
{
75+
if ($this->getConfig('script_nonce')) {
76+
$request = $request->withAttribute('cspScriptNonce', $this->csp->nonce('script-src'));
77+
}
78+
if ($this->getConfig('style_nonce')) {
79+
$request = $request->withAttribute('cspStyleNonce', $this->csp->nonce('style-src'));
80+
}
81+
82+
$response = $handler->handle($request);
83+
84+
/** @var ResponseInterface */
85+
return $this->csp->injectCSPHeader($response);
86+
}
87+
}

0 commit comments

Comments
 (0)