CSRF
The Csrf submodule protects forms and HTTP requests against Cross-Site Request Forgery attacks via session tokens.
Structure
Csrf/
├── CsrfManager.php # Single token per session
├── CsrfTokenManager.php # Named tokens with expiration
├── CsrfModule.php # DI registration
├── Exception/
│ └── CsrfException.php
├── Extension/
│ └── CsrfViewExtension.php # csrf_token() Twig function
└── Token/
└── CsrfToken.php # Token value object
CsrfManager
File: CsrfManager.php
Manages a single token per session, stored under the _csrf_token key. This is the component used by CsrfMiddleware.
$csrf = $container->get(CsrfManager::class);
// Generate or retrieve the current session's token
$token = $csrf->generate();
// Read the token without creating it
$token = $csrf->token();
// Validate the token sent in the request
$isValid = $csrf->validate();
// Force the token to regenerate
$csrf->refresh();
Token sources in the request (in order):
body('_csrf_token')— hidden field in an HTML form.header('X-CSRF-Token')— HTTP header (for AJAX requests).
Secure comparison: hash_equals() is used to prevent timing attacks.
Example in a controller:
#[Route('/profile/edit', 'POST')]
public function edit(): Response
{
// CsrfMiddleware validates automatically if configured.
// Otherwise, manual validation:
if (!$this->csrfManager->validate()) {
throw new \RuntimeException('Invalid CSRF token.');
}
// ...
}
CsrfTokenManager
File: CsrfTokenManager.php
Advanced alternative to CsrfManager. Manages named tokens with individual expiration, one per form, in parallel.
$manager = $container->get(CsrfTokenManager::class);
// Generate a token for a specific form (expires in 3600s)
$token = $manager->generateToken('contact_form', expiry: 3600);
$tokenValue = $token->getValue(); // 64-character hex string
// Retrieve an existing token
$token = $manager->getToken('contact_form'); // CsrfToken|null
// Validate and consume the token (invalidate: true = removed after validation)
$isValid = $manager->validateToken('contact_form', $submittedValue, invalidate: true);
Storage: $_SESSION['_csrf_tokens']['<id>']
Expired token: if expired at validation time, it is removed from the session and the method returns false.
CLI behavior: in a CLI context (PHP_SAPI === 'cli'), every operation is a silent no-op.
CsrfToken
File: Token/CsrfToken.php
Value object representing a named token.
$token->getId(); // 'contact_form'
$token->getValue(); // 64-character hexadecimal string (32 bytes)
$token->getExpiry(); // Unix expiration timestamp
$token->isExpired(); // true if time() > expiry
Twig Extension
File: Extension/CsrfViewExtension.php
Exposes the csrf_token() function in every Twig template. If the token does not yet exist in the session, it is created automatically.
{# Default token (id 'default') #}
<form method="POST" action="{{ path('profile.edit') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
{# ... form fields ... #}
<button type="submit">Save</button>
</form>
{# Named token for a specific form #}
<input type="hidden" name="_csrf_token" value="{{ csrf_token('contact_form') }}">
CsrfMiddleware
File: ../Middleware/Default/CsrfMiddleware.php
Automatically validates the CSRF token for every non-safe request. GET, HEAD, and OPTIONS methods are ignored.
use Neo\Core\Security\Middleware\Attribute\Middleware;
use Neo\Core\Security\Middleware\Default\CsrfMiddleware;
// On an entire controller
#[Middleware(use: CsrfMiddleware::class, message: 'Missing or invalid CSRF token.')]
class MyController extends AbstractController { ... }
// On a specific method
#[Middleware(use: CsrfMiddleware::class)]
#[Route('/settings', 'POST')]
public function update(): Response { ... }
See Middleware/README.md for the full pipeline configuration.