Switch to named CSRF tokens

This commit is contained in:
Giuseppe Criscione 2024-06-16 14:55:53 +02:00
parent a45ee5d4a3
commit 12dd4abaa7
6 changed files with 48 additions and 23 deletions

View File

@ -102,7 +102,7 @@ abstract class AbstractController extends BaseAbstractController
'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' => [

View File

@ -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));
}
@ -33,7 +35,7 @@ class AuthenticationController extends AbstractController
}
// Always generate a new CSRF token
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
return new Response($this->view('authentication.login', [
'title' => $this->translate('panel.login.login'),
@ -47,7 +49,7 @@ class AuthenticationController extends AbstractController
// Ensure no required data is missing
if (!$data->hasMultiple(['username', 'password'])) {
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
$this->error($this->translate('panel.login.attempt.failed'));
}
@ -61,7 +63,7 @@ class AuthenticationController extends AbstractController
$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'));
@ -79,7 +81,7 @@ class AuthenticationController extends AbstractController
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,
@ -94,7 +96,7 @@ class AuthenticationController extends AbstractController
*/
public function logout(Request $request, CsrfToken $csrfToken): RedirectResponse
{
$csrfToken->destroy();
$csrfToken->destroy($this->panel()->getCsrfTokenName());
$request->session()->remove('FORMWORK_USERNAME');
$request->session()->destroy();

View File

@ -26,7 +26,7 @@ class RegisterController extends AbstractController
return $this->redirectToReferer();
}
$csrfToken->generate();
$csrfToken->generate($this->panel()->getCsrfTokenName());
$fields = $schemes->get('forms.register')->fields();

View File

@ -15,6 +15,8 @@ use Formwork\Utils\Uri;
final class Panel
{
protected const CSRF_TOKEN_NAME = 'panel';
/**
* Assets instance
*/
@ -178,4 +180,12 @@ final class Panel
return $translations;
}
/**
* Get panel CSRF token name
*/
public function getCsrfTokenName(): string
{
return self::CSRF_TOKEN_NAME;
}
}

View File

@ -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)
{
@ -18,34 +18,45 @@ class CsrfToken
/**
* 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);
}
}

View File

@ -223,7 +223,7 @@ return [
],
'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) {
@ -239,14 +239,16 @@ return [
}
},
'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(