From 23c0fae36fb8159bcf8b95bae98555201146457e Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Mon, 3 Sep 2018 15:33:13 +0100 Subject: Added csrf middleware --- src/Http/Response.php | 12 ++++- src/Http/SessionServiceProvider.php | 7 +++ src/Middleware/VerifyCsrfToken.php | 90 +++++++++++++++++++++++++++++++++++ src/Renderer/Twig/Extensions/Csrf.php | 48 +++++++++++++++++++ src/Renderer/TwigServiceProvider.php | 4 +- 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/Middleware/VerifyCsrfToken.php create mode 100644 src/Renderer/Twig/Extensions/Csrf.php (limited to 'src') diff --git a/src/Http/Response.php b/src/Http/Response.php index 4edf644a..58cd7662 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -96,7 +96,7 @@ class Response extends SymfonyResponse implements ResponseInterface /** * Return an instance with the rendered content. * - * THis method retains the immutability of the message and returns + * This method retains the immutability of the message and returns * an instance with the updated status and headers * * @param string $view @@ -111,6 +111,14 @@ class Response extends SymfonyResponse implements ResponseInterface throw new \InvalidArgumentException('Renderer not defined'); } - return $this->create($this->view->render($view, $data), $status, $headers); + $new = clone $this; + $new->setContent($this->view->render($view, $data)); + $new->setStatusCode($status, ($status == $this->getStatusCode() ? $this->statusText : null)); + + foreach ($headers as $key => $values) { + $new = $new->withAddedHeader($key, $values); + } + + return $new; } } diff --git a/src/Http/SessionServiceProvider.php b/src/Http/SessionServiceProvider.php index c2e09624..4d779aa6 100644 --- a/src/Http/SessionServiceProvider.php +++ b/src/Http/SessionServiceProvider.php @@ -5,7 +5,9 @@ namespace Engelsystem\Http; use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; use Engelsystem\Http\SessionHandlers\DatabaseHandler; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; @@ -21,6 +23,11 @@ class SessionServiceProvider extends ServiceProvider $session = $this->app->make(Session::class); $this->app->instance(Session::class, $session); $this->app->instance('session', $session); + $this->app->bind(SessionInterface::class, Session::class); + + if (!$session->has('_token')) { + $session->set('_token', Str::random(42)); + } /** @var Request $request */ $request = $this->app->get('request'); diff --git a/src/Middleware/VerifyCsrfToken.php b/src/Middleware/VerifyCsrfToken.php new file mode 100644 index 00000000..cc0c1fbc --- /dev/null +++ b/src/Middleware/VerifyCsrfToken.php @@ -0,0 +1,90 @@ +session = $session; + } + + /** + * Verify csrf tokens + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ( + $this->isReading($request) + || $this->tokensMatch($request) + ) { + return $handler->handle($request); + } + + return $this->notAuthorizedResponse(); + } + + /** + * @param ServerRequestInterface $request + * @return bool + */ + protected function isReading(ServerRequestInterface $request): bool + { + return in_array( + $request->getMethod(), + ['GET', 'HEAD', 'OPTIONS'] + ); + } + + /** + * @param ServerRequestInterface $request + * @return bool + */ + protected function tokensMatch(ServerRequestInterface $request): bool + { + $token = null; + $body = $request->getParsedBody(); + $header = $request->getHeader('X-CSRF-TOKEN'); + + if (is_array($body) && isset($body['_token'])) { + $token = $body['_token']; + } + + if (!empty($header)) { + $header = array_shift($header); + } + + $token = $token ?: $header; + $sessionToken = $this->session->get('_token'); + + return is_string($token) + && is_string($sessionToken) + && hash_equals($sessionToken, $token); + } + + /** + * @return ResponseInterface + * @codeCoverageIgnore + */ + protected function notAuthorizedResponse(): ResponseInterface + { + // The 419 code is used as "Page Expired" to differentiate from a 401 (not authorized) + return response()->withStatus(419, 'Authentication Token Mismatch'); + } +} diff --git a/src/Renderer/Twig/Extensions/Csrf.php b/src/Renderer/Twig/Extensions/Csrf.php new file mode 100644 index 00000000..9f77df80 --- /dev/null +++ b/src/Renderer/Twig/Extensions/Csrf.php @@ -0,0 +1,48 @@ +session = $session; + } + + /** + * @return TwigFunction[] + */ + public function getFunctions() + { + return [ + new TwigFunction('csrf', [$this, 'getCsrfField'], ['is_safe' => ['html']]), + new TwigFunction('csrf_token', [$this, 'getCsrfToken']), + ]; + } + + /** + * @return string + */ + public function getCsrfField() + { + return sprintf('', $this->getCsrfToken()); + } + + /** + * @return string + */ + public function getCsrfToken() + { + return $this->session->get('_token'); + } +} diff --git a/src/Renderer/TwigServiceProvider.php b/src/Renderer/TwigServiceProvider.php index 49a0eb90..57ebe9e5 100644 --- a/src/Renderer/TwigServiceProvider.php +++ b/src/Renderer/TwigServiceProvider.php @@ -4,9 +4,10 @@ namespace Engelsystem\Renderer; use Engelsystem\Config\Config as EngelsystemConfig; use Engelsystem\Container\ServiceProvider; -use Engelsystem\Renderer\Twig\Extensions\Authentication; use Engelsystem\Renderer\Twig\Extensions\Assets; +use Engelsystem\Renderer\Twig\Extensions\Authentication; use Engelsystem\Renderer\Twig\Extensions\Config; +use Engelsystem\Renderer\Twig\Extensions\Csrf; use Engelsystem\Renderer\Twig\Extensions\Globals; use Engelsystem\Renderer\Twig\Extensions\Legacy; use Engelsystem\Renderer\Twig\Extensions\Session; @@ -23,6 +24,7 @@ class TwigServiceProvider extends ServiceProvider 'assets' => Assets::class, 'authentication' => Authentication::class, 'config' => Config::class, + 'csrf' => Csrf::class, 'globals' => Globals::class, 'session' => Session::class, 'legacy' => Legacy::class, -- cgit v1.2.3-70-g09d2