Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions formwork/routes.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
<?php

use Formwork\Config\Config;
use Formwork\ErrorHandlers;
use Formwork\Http\RedirectResponse;
use Formwork\Http\Request;
use Formwork\Http\ResponseStatus;
use Formwork\Languages\Languages;
use Formwork\Router\Router;
use Formwork\Security\CsrfToken;
use Formwork\Utils\FileSystem;
use Formwork\Utils\Str;

return [
'routes' => [
'index' => [
'path' => '/',
'action' => 'Formwork\Controllers\PageController@load',
'path' => '/',
'action' => 'Formwork\Controllers\PageController@load',
'methods' => ['GET', 'POST'],
],
'index.pagination' => [
'path' => '/page/{paginationPage:num}/',
Expand All @@ -34,12 +39,50 @@
'action' => 'Formwork\Controllers\PageController@load',
],
'page' => [
'path' => '/{page}/',
'action' => 'Formwork\Controllers\PageController@load',
'path' => '/{page}/',
'action' => 'Formwork\Controllers\PageController@load',
'methods' => ['GET', 'POST'],
],
],

'filters' => [
'request.validateSize' => [
'action' => static function (Config $config, Request $request, Router $router, ErrorHandlers $errorHandlers) {
if ($config->get('system.panel.enabled') && $router->requestHasPrefix($config->get('system.panel.root'))) {
return;
}

// Validate HTTP request Content-Length according to `post_max_size` directive
if ($request->contentLength() !== null) {
$maxSize = FileSystem::shorthandToBytes(ini_get('post_max_size') ?: '0');

if ($request->contentLength() > $maxSize && $maxSize > 0) {
$errorHandlers->displayErrorPage(ResponseStatus::PayloadTooLarge);
}
}
},
'methods' => ['POST'],
'types' => ['HTTP', 'XHR'],
],

'request.validateCsrf' => [
'action' => static function (Config $config, Request $request, Router $router, CsrfToken $csrfToken, ErrorHandlers $errorHandlers) {
if ($config->get('system.panel.enabled') && $router->requestHasPrefix($config->get('system.panel.root'))) {
return;
}

$tokenName = (string) $request->input()->get('csrf-token-name', 'site');
$token = (string) $request->input()->get('csrf-token');

if (!($csrfToken->validate($tokenName, $token))) {
$csrfToken->destroy($tokenName);
$errorHandlers->displayErrorPage(ResponseStatus::Forbidden);
}
},
'methods' => ['POST'],
'types' => ['HTTP', 'XHR'],
],

'language' => [
'action' => function (Config $config, Request $request, Router $router, Languages $languages) {
if (($requested = $languages->requested()) !== null) {
Expand Down
3 changes: 2 additions & 1 deletion formwork/src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ protected function loadServices(Container $container): void
->loader(ErrorHandlersServiceLoader::class)
->lazy(!$this->config()->get('system.errors.setHandlers', true));

$container->define(CsrfToken::class);
$container->define(CsrfToken::class)
->alias('csrfToken');

$container->define(Router::class)
->alias('router');
Expand Down
6 changes: 3 additions & 3 deletions formwork/src/Pages/Templates/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Closure;
use Formwork\App;
use Formwork\Assets;
use Formwork\Pages\Page;
use Formwork\Pages\Site;
use Formwork\Utils\Constraint;
use Formwork\Utils\FileSystem;
Expand Down Expand Up @@ -98,8 +97,9 @@ public function render(array $vars = []): string
protected function defaultVars(): array
{
return [
'router' => $this->app->router(),
'site' => $this->site,
'router' => $this->app->router(),
'site' => $this->site,
'csrfToken' => $this->app->getService('csrfToken'),
];
}

Expand Down
2 changes: 1 addition & 1 deletion formwork/src/Panel/Controllers/AbstractController.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ protected function defaults(): array
'location' => $this->name,
'site' => $this->site(),
'panel' => $this->panel(),
'csrfToken' => $this->csrfToken->get(),
'csrfToken' => $this->csrfToken->get($this->panel()->getCsrfTokenName()),
'modals' => $this->modals(),
'colorScheme' => $this->getColorScheme(),
'navigation' => [
Expand Down
14 changes: 8 additions & 6 deletions formwork/src/Panel/Controllers/AuthenticationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ class AuthenticationController extends AbstractController
*/
public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $accessLimiter): Response
{
$csrfTokenName = $this->panel()->getCsrfTokenName();

if ($accessLimiter->hasReachedLimit()) {
$minutes = round($this->config->get('system.panel.loginResetTime') / 60);
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
return $this->error($this->translate('panel.login.attempt.tooMany', $minutes));
}

Expand All @@ -33,7 +35,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
}

// Always generate a new CSRF token
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);

return new Response($this->view('authentication.login', [
'title' => $this->translate('panel.login.login'),
Expand All @@ -47,7 +49,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc

// Ensure no required data is missing
if (!$data->hasMultiple(['username', 'password'])) {
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
$this->error($this->translate('panel.login.attempt.failed'));
}

Expand All @@ -61,7 +63,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
$request->session()->set('FORMWORK_USERNAME', $data->get('username'));

// Regenerate CSRF token
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);

$accessLog = new Log(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'access.json'));
$lastAccessRegistry = new Registry(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'lastAccess.json'));
Expand All @@ -79,7 +81,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
return $this->redirect($this->generateRoute('panel.index'));
}

$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
return $this->error($this->translate('panel.login.attempt.failed'), [
'username' => $data->get('username'),
'error' => true,
Expand All @@ -94,7 +96,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
*/
public function logout(Request $request, CsrfToken $csrfToken): RedirectResponse
{
$csrfToken->destroy();
$csrfToken->destroy($this->panel()->getCsrfTokenName());
$request->session()->remove('FORMWORK_USERNAME');
$request->session()->destroy();

Expand Down
2 changes: 1 addition & 1 deletion formwork/src/Panel/Controllers/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function register(Request $request, Schemes $schemes, CsrfToken $csrfToke
return $this->redirectToReferer();
}

$csrfToken->generate();
$csrfToken->generate($this->panel()->getCsrfTokenName());

$fields = $schemes->get('forms.register')->fields();

Expand Down
10 changes: 10 additions & 0 deletions formwork/src/Panel/Panel.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

final class Panel
{
protected const CSRF_TOKEN_NAME = 'panel';

/**
* Assets instance
*/
Expand Down Expand Up @@ -178,4 +180,12 @@ public function availableTranslations(): array

return $translations;
}

/**
* Get panel CSRF token name
*/
public function getCsrfTokenName(): string
{
return self::CSRF_TOKEN_NAME;
}
}
8 changes: 8 additions & 0 deletions formwork/src/Router/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ public function request(): string
return $this->requestUri;
}

/**
* Check if the request has the given prefix
*/
public function requestHasPrefix(string $prefix): bool
{
return $this->matchPrefix($prefix);
}

public function setRequest(string $request): void
{
$requestPath = Uri::path($request) ?? throw new UnexpectedValueException('Cannot get request path');
Expand Down
31 changes: 21 additions & 10 deletions formwork/src/Security/CsrfToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class CsrfToken
/**
* Session key to store the CSRF token
*/
protected const SESSION_KEY = 'CSRF_TOKEN';
protected const SESSION_KEY_PREFIX = '_formwork_csrf_tokens';

public function __construct(protected Request $request)
{
Expand All @@ -18,34 +18,45 @@ public function __construct(protected Request $request)
/**
* Generate a new CSRF token
*/
public function generate(): string
public function generate(string $name): string
{
$token = base64_encode(random_bytes(36));
$this->request->session()->set(self::SESSION_KEY, $token);
$this->request->session()->set(self::SESSION_KEY_PREFIX . '.' . $name, $token);
return $token;
}

/**
* Get current CSRF token
* Check if CSRF token exists
*/
public function get(): ?string
public function has(string $name): bool
{
return $this->request->session()->get(self::SESSION_KEY);
return $this->request->session()->has(self::SESSION_KEY_PREFIX . '.' . $name);
}

/**
* Get CSRF token by name
*/
public function get(string $name, bool $autoGenerate = false): ?string
{
if ($autoGenerate && !$this->has($name)) {
return $this->generate($name);
}
return $this->request->session()->get(self::SESSION_KEY_PREFIX . '.' . $name);
}

/**
* Check if given CSRF token is valid
*/
public function validate(string $token): bool
public function validate(string $name, string $token): bool
{
return ($storedToken = $this->get()) && hash_equals($token, $storedToken);
return ($storedToken = $this->get($name)) && hash_equals($token, $storedToken);
}

/**
* Remove CSRF token from session data
*/
public function destroy(): void
public function destroy(string $name): void
{
$this->request->session()->remove(self::SESSION_KEY);
$this->request->session()->remove(self::SESSION_KEY_PREFIX . '.' . $name);
}
}
12 changes: 7 additions & 5 deletions panel/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
],

'filters' => [
'request.validateSize' => [
'panel.request.validateSize' => [
'action' => static function (Request $request, Translations $translations, Panel $panel) {
// Validate HTTP request Content-Length according to `post_max_size` directive
if ($request->contentLength() !== null) {
Expand All @@ -239,14 +239,16 @@
}
},
'methods' => ['POST'],
'types' => ['HTTP', 'XHR'],
],

'request.validateCsrf' => [
'panel.request.validateCsrf' => [
'action' => static function (Request $request, Translations $translations, Panel $panel, CsrfToken $csrfToken) {
$token = $request->input()->get('csrf-token');
$tokenName = $panel->getCsrfTokenName();
$token = (string) $request->input()->get('csrf-token');

if (!($token !== null && $csrfToken->validate($token))) {
$csrfToken->destroy();
if (!$csrfToken->validate($tokenName, $token)) {
$csrfToken->destroy($tokenName);
$request->session()->remove('FORMWORK_USERNAME');

$panel->notify(
Expand Down