Framework Documentation - Application Package
Session and CSRF
SessionAwareWebApplicationTrait implements SessionAwareWebApplicationInterface and gives an
application a session plus form-token handling. WebApplication already uses the trait.
Attaching a session
use Joomla\Session\Session;
use Joomla\Session\Storage\NativeStorage;
use Joomla\Session\Handler\FilesystemHandler;
$session = new Session(new NativeStorage(new FilesystemHandler(), [
'name' => 'app_session',
'use_strict_mode' => 1, // reject client supplied session ids
'cookie_httponly' => 1,
'cookie_secure' => 1, // set only when you serve over HTTPS
'gc_maxlifetime' => 1440,
]));
$app->setSession($session);
$session->start();getSession() throws a RuntimeException if no session was set, so the application fails loudly
rather than silently skipping session-dependent code.
The session package does not apply secure defaults on its own.
use_strict_mode,
cookie_httponlyandcookie_secureall default to whateverphp.inisays. Set them explicitly
as shown above.cookie_samesiteis not in the option allowlist ofNativeStorage::setOptions()
and has to be set withini_set('session.cookie_samesite', 'Lax')before the session starts.
Form tokens
The token is a random value stored in the session under session.token:
$token = $app->getFormToken(); // creates one on first call
$token = $app->getFormToken(true); // force a new one, e.g. after loginThere are two ways to submit it, and checkToken() accepts both.
HTML forms — the token is the field name
<form method="post" action="/articles">
<input type="hidden" name="<?= $app->getFormToken() ?>" value="1">
…
</form>The secret is the field name. An attacker who cannot read the page cannot guess it, so the request
cannot be forged.
JavaScript — the token is the header value
fetch('/articles', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});with the token rendered into the page:
<meta name="csrf-token" content="<?= $app->getFormToken() ?>">Checking the token
if (!$app->checkToken()) {
$app->setHeader('Status', '403', true);
$app->setBody('Invalid or missing CSRF token.');
return;
}checkToken($method = 'post'):
- rejects any
$methodother thanpostorgetwith anInvalidArgumentException, - reads
X-CSRF-Tokenunfiltered; if present, that value is the submitted token, - otherwise checks whether a request variable named like the session token exists,
- compares the submitted value against the session token with
hash_equals(), - asks the session about the result, which expires the session when the check fails,
- returns the boolean.
Because a failed check expires the session, a forged request also invalidates the victim's session
rather than merely being rejected.
Changed behaviour. Before this was fixed, the
X-CSRF-Tokenpath only tested that the header
was present; its value was never compared to the session token. Any request carrying
X-CSRF-Token: apassed the check. If you have code or tests that relied on that, they need
updating — the header must now carry the real token.
Where to check
Check on every state-changing request. The cleanest place is a BEFORE_EXECUTE listener so no
controller can forget:
use Joomla\Application\ApplicationEvents;
use Joomla\Application\Event\ApplicationEvent;
$dispatcher->addListener(
ApplicationEvents::BEFORE_EXECUTE,
static function (ApplicationEvent $event): void {
/** @var \Joomla\Application\WebApplication $app */
$app = $event->getApplication();
if (\in_array($app->getInput()->getMethod(), ['GET', 'HEAD', 'OPTIONS'], true)) {
return;
}
if (!$app->checkToken()) {
throw new \RuntimeException('Invalid CSRF token', 403);
}
}
);The exception is caught by execute() and turned into an application.error event, where your
error listener renders the 403 — see Lifecycle and events.
Session fixation
Regenerate the session id whenever the privilege level changes — most importantly right after a
successful login. Neither this package nor joomla/authentication does it for you:
if ($authentication->authenticate() !== false) {
$app->getSession()->fork(true); // new id, destroy the old session
$app->getSession()->set('user.id', $userId);
$app->getFormToken(true); // and a fresh CSRF token
}