Skip to content

Commit 16bc55b

Browse files
authored
Merge pull request #58 from blitz-php/devs
feat: ajout du middleware de protection CSRF
2 parents 1b1a848 + 7964c45 commit 16bc55b

12 files changed

Lines changed: 230 additions & 31 deletions

src/Debug/ExceptionManager.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace BlitzPHP\Debug;
1313

14+
use BlitzPHP\Exceptions\HttpException;
15+
use BlitzPHP\Exceptions\TokenMismatchException;
1416
use BlitzPHP\View\View;
1517
use Symfony\Component\Finder\SplFileInfo;
1618
use Throwable;
@@ -35,6 +37,8 @@ class ExceptionManager
3537
public static function registerHttpErrors(Run $debugger, array $config): Run
3638
{
3739
return $debugger->pushHandler(static function (Throwable $exception, InspectorInterface $inspector, RunInterface $run) use ($config): int {
40+
$exception = self::prepareException($exception);
41+
3842
$exception_code = $exception->getCode();
3943
if ($exception_code >= 400 && $exception_code < 600) {
4044
$run->sendHttpCode($exception_code);
@@ -52,7 +56,7 @@ public static function registerHttpErrors(Run $debugger, array $config): Run
5256

5357
if (in_array((string) $exception->getCode(), $files, true)) {
5458
$view = new View();
55-
$view->setAdapter(config('view.active_adapter', 'native'), ['view_path_locator' => $config['error_view_path']])
59+
$view->setAdapter(config('view.active_adapter', 'native'), ['view_path' => $config['error_view_path']])
5660
->display((string) $exception->getCode())
5761
->setData(['message' => $exception->getMessage()])
5862
->render();
@@ -166,4 +170,16 @@ private static function setBlacklist(PrettyPageHandler $handler, array $blacklis
166170

167171
return $handler;
168172
}
173+
174+
/**
175+
* Prepare exception for rendering.
176+
*/
177+
private static function prepareException(Throwable $e): Throwable
178+
{
179+
if ($e instanceof TokenMismatchException) {
180+
$e = new HttpException($e->getMessage(), 419, $e);
181+
}
182+
183+
return $e;
184+
}
169185
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\Exceptions;
13+
14+
class TokenMismatchException extends FrameworkException
15+
{
16+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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\Contracts\Http\ResponsableInterface;
15+
use BlitzPHP\Contracts\Security\EncrypterInterface;
16+
use BlitzPHP\Exceptions\EncryptionException;
17+
use BlitzPHP\Exceptions\TokenMismatchException;
18+
use BlitzPHP\Http\Request;
19+
use BlitzPHP\Http\Response;
20+
use BlitzPHP\Session\Cookie\Cookie;
21+
use BlitzPHP\Session\Cookie\CookieValuePrefix;
22+
use BlitzPHP\Traits\Support\InteractsWithTime;
23+
use Psr\Http\Message\ResponseInterface;
24+
use Psr\Http\Message\ServerRequestInterface;
25+
use Psr\Http\Server\MiddlewareInterface;
26+
use Psr\Http\Server\RequestHandlerInterface;
27+
28+
class VerifyCsrfToken implements MiddlewareInterface
29+
{
30+
use InteractsWithTime;
31+
32+
/**
33+
* Les URI qui doivent être exclus de la vérification CSRF.
34+
*/
35+
protected array $except = [];
36+
37+
/**
38+
* Indique si le cookie XSRF-TOKEN doit être défini dans la réponse.
39+
*/
40+
protected bool $addHttpCookie = true;
41+
42+
/**
43+
* Constructeur
44+
*/
45+
public function __construct(protected EncrypterInterface $encrypter)
46+
{
47+
}
48+
49+
/**
50+
* {@inheritDoc}
51+
*
52+
* @param Request $request
53+
*/
54+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
55+
{
56+
if ($this->isReading($request) || $this->runningUnitTests() || $this->inExceptArray($request) || $this->tokensMatch($request)) {
57+
return tap($handler->handle($request), function ($response) use ($request) {
58+
if ($this->shouldAddXsrfTokenCookie()) {
59+
$this->addCookieToResponse($request, $response);
60+
}
61+
});
62+
}
63+
64+
throw new TokenMismatchException('Erreur de jeton CSRF.');
65+
}
66+
67+
/**
68+
* Détermine si la requête HTTP utilise un verbe « read ».
69+
*/
70+
protected function isReading(Request $request): bool
71+
{
72+
return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS'], true);
73+
}
74+
75+
/**
76+
* Détermine si l'application exécute des tests unitaires.
77+
*/
78+
protected function runningUnitTests(): bool
79+
{
80+
return is_cli() && on_test();
81+
}
82+
83+
/**
84+
* Détermine si la requête comporte un URI qui doit faire l'objet d'une vérification CSRF.
85+
*/
86+
protected function inExceptArray(Request $request): bool
87+
{
88+
foreach ($this->except as $except) {
89+
if ($except !== '/') {
90+
$except = trim($except, '/');
91+
}
92+
93+
if ($request->fullUrlIs($except) || $request->pathIs($except)) {
94+
return true;
95+
}
96+
}
97+
98+
return false;
99+
}
100+
101+
/**
102+
* Détermine si les jetons CSRF de session et d'entrée correspondent.
103+
*/
104+
protected function tokensMatch(Request $request): bool
105+
{
106+
$token = $this->getTokenFromRequest($request);
107+
108+
return is_string($request->session()->token())
109+
&& is_string($token)
110+
&& hash_equals($request->session()->token(), $token);
111+
}
112+
113+
/**
114+
* Récupère le jeton CSRF de la requête.
115+
*/
116+
protected function getTokenFromRequest(Request $request): ?string
117+
{
118+
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
119+
120+
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
121+
try {
122+
$token = CookieValuePrefix::remove($this->encrypter->decrypt($header));
123+
} catch (EncryptionException) {
124+
$token = '';
125+
}
126+
}
127+
128+
return $token;
129+
}
130+
131+
/**
132+
* Détermine si le cookie doit être ajouté à la réponse.
133+
*/
134+
public function shouldAddXsrfTokenCookie(): bool
135+
{
136+
return $this->addHttpCookie;
137+
}
138+
139+
/**
140+
* Ajoute le jeton CSRF aux cookies de la réponse.
141+
*
142+
* @param Response $response
143+
*/
144+
protected function addCookieToResponse(Request $request, $response): ResponseInterface
145+
{
146+
if ($response instanceof ResponsableInterface) {
147+
$response = $response->toResponse($request);
148+
}
149+
150+
if (! ($response instanceof Response)) {
151+
return $response;
152+
}
153+
154+
$config = config('cookie');
155+
156+
return $response->withCookie(Cookie::create('XSRF-TOKEN', $request->session()->token(), [
157+
'expires' => $this->availableAt(config('session.expiration')),
158+
'path' => $config['path'],
159+
'domain' => $config['domain'],
160+
'secure' => $config['secure'],
161+
'httponly' => false,
162+
'samesite' => $config['samesite'] ?? null,
163+
]));
164+
}
165+
166+
/**
167+
* Détermine si le contenu du cookie doit être sérialisé.
168+
*/
169+
public static function serialized(): bool
170+
{
171+
return EncryptCookies::serialized('XSRF-TOKEN');
172+
}
173+
}

src/View/Adapters/AbstractAdapter.php

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ abstract class AbstractAdapter implements RendererInterface
5050
/**
5151
* Instance de Locator lorsque nous devons tenter de trouver une vue qui n'est pas à l'emplacement standard.
5252
*/
53-
protected ?LocatorInterface $locator = null;
53+
protected LocatorInterface $locator;
5454

5555
/**
5656
* Le nom de la mise en page utilisée, le cas échéant.
@@ -70,25 +70,19 @@ abstract class AbstractAdapter implements RendererInterface
7070
/**
7171
* {@inheritDoc}
7272
*
73-
* @param array $config Configuration actuelle de l'adapter
74-
* @param bool $debug Devrions-nous stocker des informations sur les performances ?
73+
* @param array $config Configuration actuelle de l'adapter
74+
* @param string|null $viewPath Dossier principal dans lequel les vues doivent être cherchées
75+
* @param bool $debug Devrions-nous stocker des informations sur les performances ?
7576
*/
76-
public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
77+
public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG)
7778
{
7879
helper('assets');
7980

80-
if (! empty($viewPathLocator)) {
81-
if (is_string($viewPathLocator)) {
82-
$this->viewPath = rtrim($viewPathLocator, '\\/ ') . DS;
83-
} elseif ($viewPathLocator instanceof LocatorInterface) {
84-
$this->locator = $viewPathLocator;
85-
}
81+
if (is_string($viewPath) && is_dir($viewPath = rtrim($viewPath, '\\/ ') . DS)) {
82+
$this->viewPath = $viewPath;
8683
}
8784

88-
if (! $this->locator instanceof LocatorInterface && ! is_dir($this->viewPath)) {
89-
$this->viewPath = '';
90-
$this->locator = service('locator');
91-
}
85+
$this->locator = service('locator');
9286

9387
$this->ext = preg_replace('#^\.#', '', $config['extension'] ?? $this->ext);
9488
}
@@ -264,7 +258,7 @@ protected function getRenderedFile(?array $options, string $view, ?string $ext =
264258

265259
$file = Helpers::ensureExt($file, $ext);
266260

267-
if (! is_file($file) && $this->locator instanceof LocatorInterface) {
261+
if (! is_file($file)) {
268262
$file = $this->locator->locateFile($view, 'Views', $ext);
269263
}
270264

src/View/Adapters/BladeAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ class BladeAdapter extends AbstractAdapter
3030
/**
3131
* {@inheritDoc}
3232
*/
33-
public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
33+
public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG)
3434
{
35-
parent::__construct($config, $viewPathLocator, $debug);
35+
parent::__construct($config, $viewPath, $debug);
3636

3737
$this->engine = new Blade(
3838
$this->viewPath ?: VIEW_PATH,

src/View/Adapters/LatteAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ class LatteAdapter extends AbstractAdapter
3131
/**
3232
* {@inheritDoc}
3333
*/
34-
public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
34+
public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG)
3535
{
36-
parent::__construct($config, $viewPathLocator, $debug);
36+
parent::__construct($config, $viewPath, $debug);
3737

3838
$this->latte = new Engine();
3939

src/View/Adapters/NativeAdapter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ class NativeAdapter extends AbstractAdapter
7878
/**
7979
* {@inheritDoc}
8080
*/
81-
public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
81+
public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG)
8282
{
83-
parent::__construct($config, $viewPathLocator, $debug);
83+
parent::__construct($config, $viewPath, $debug);
8484

8585
$this->saveData = (bool) ($config['save_data'] ?? true);
8686
}
@@ -591,7 +591,7 @@ public function required(bool|string $condition): string
591591
/**
592592
* Génère un champ input caché à utiliser dans les formulaires générés manuellement.
593593
*/
594-
public function csrf(?string $id): string
594+
public function csrf(?string $id = null): string
595595
{
596596
return csrf_field($id);
597597
}

src/View/Adapters/PlatesAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ class PlatesAdapter extends AbstractAdapter
3131
/**
3232
* {@inheritDoc}
3333
*/
34-
public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
34+
public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG)
3535
{
36-
parent::__construct($config, $viewPathLocator, $debug);
36+
parent::__construct($config, $viewPath, $debug);
3737

3838
$this->engine = new Engine(rtrim($this->viewPath, '/\\'), $this->ext);
3939

src/View/Adapters/SmartyAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ class SmartyAdapter extends AbstractAdapter
3030
/**
3131
* {@inheritDoc}
3232
*/
33-
public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
33+
public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG)
3434
{
35-
parent::__construct($config, $viewPathLocator, $debug);
35+
parent::__construct($config, $viewPath, $debug);
3636

3737
$this->engine = new Smarty();
3838

src/View/Adapters/TwigAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ class TwigAdapter extends AbstractAdapter
3333
/**
3434
* {@inheritDoc}
3535
*/
36-
public function __construct(protected array $config, $viewPathLocator = null, protected bool $debug = BLITZ_DEBUG)
36+
public function __construct(protected array $config, $viewPath = null, protected bool $debug = BLITZ_DEBUG)
3737
{
38-
parent::__construct($config, $viewPathLocator, $debug);
38+
parent::__construct($config, $viewPath, $debug);
3939

4040
$loader = new FilesystemLoader([
4141
$this->viewPath,

0 commit comments

Comments
 (0)