Synchronizer tokens, SameSite, and Magento's form_key working together
Running forms and state-changing endpoints without real CSRF protection means relying solely on the browser's session cookie, opening the door to forged requests from other sites. Synchronizer tokens, double-submit cookies, the SameSite attribute, and Magento's form_key show how to close that gap reliably.
Table of Contents
- 1. What CSRF is and how it exploits automatic cookie inclusion
- 2. Anatomy of a CSRF attack
- 3. The synchronizer token pattern: mechanics and validation
- 4. Double-submit cookie as a stateless approach
- 5. The SameSite cookie attribute as a complementary defense
- 6. Why GET requests must never have side effects
- 7. Magento's form_key CSRF protection in detail
- 8. CSRF protection for AJAX, SPAs, and API endpoints
- 9. CSRF defense mechanisms compared
- 10. Summary
- 11. FAQ
1. What CSRF is and how it exploits automatic cookie inclusion
Cross-Site Request Forgery (CSRF) exploits a fundamental browser behavior that normal usage depends on: on every request to a domain, the browser automatically attaches all matching cookies, regardless of which page triggered the request. If a user is logged into an application and holds a valid session cookie, the browser sends that cookie along even when the request originates from a completely unrelated website. The server ultimately just sees an authenticated request and, without additional checks, cannot tell whether the user deliberately triggered it.
The crucial difference from Cross-Site Scripting (XSS): the attacker never needs to read or steal the session cookie. The same-origin policy prevents foreign JavaScript from reading the response of another domain, but it does not prevent a request from being sent in the first place. It is precisely this gap between "reading a response" and "sending a request" that makes CSRF a distinct threat requiring its own defense strategy, independent of XSS mitigations like Content Security Policy or output escaping.
2. Anatomy of a CSRF attack
A typical CSRF attack unfolds conceptually in three steps, without the target application needing a vulnerability like XSS at all. First: a victim logs into a target application, for example an online store's admin backend, and receives a session cookie. Second: while the session is still active, the victim visits a page controlled or compromised by an attacker. Third: that page contains a hidden form or an automatically firing request pointing at a state-changing URL of the target application, for example an endpoint that changes an email address or creates a new admin user.
As soon as the victim's browser fires this request, it automatically attaches the valid session cookie, and the target application's server carries out the action on the victim's behalf without the victim noticing anything. Important for understanding this: this walkthrough describes the attack pattern purely conceptually, to help frame defenses, not as a working how-to. What matters is that cookie-based authentication alone can never confirm that a request was deliberately initiated by the user.
3. The synchronizer token pattern: mechanics and validation
The synchronizer token pattern is the most established CSRF defense. The server generates a cryptographically random, unpredictable token, stores it server-side bound to the active session, and embeds it as a hidden field in every form. When the form is submitted, the browser sends the token along as part of the request body. Because the attacker neither knows this token nor can read it from a foreign page, the same-origin policy prevents exactly that, they cannot forge a valid request, even though the session cookie gets attached automatically.
Correct server-side validation is decisive: the submitted token must be compared against the stored value using a constant-time comparison (such as hash_equals() in PHP), never a simple ==, which would be vulnerable to timing attacks. The token should be regenerated on every login and, ideally, rotated after successful use to make replay attacks via stolen logs or browser history harder. If validation is missing on even a single state-changing endpoint, the entire protection for that path is worthless.
<!-- Hidden CSRF token field embedded in a state-changing form -->
<form method="POST" action="/account/email/update">
<label for="email">New email address</label>
<input type="email" id="email" name="email" required>
<!-- Synchronizer token bound to the current server-side session -->
<input type="hidden" name="csrf_token" value="a1b2c3d4e5f6...">
<button type="submit">Update email</button>
</form>
4. Double-submit cookie as a stateless approach
The double-submit cookie pattern solves the same problem without server-side session storage for the token. The server sets a random token as a cookie, and the same token must additionally be sent as a request parameter or custom header. The server only checks whether both values match. Because an attacker cannot read a foreign domain's cookie value due to the same-origin policy in order to copy it into the parameter, a forged request fails on the mismatch. This approach works especially well for stateless APIs and distributed systems where no central session store exists.
The simple variant has a known weakness: if an attacker can set a cookie with the same name via a subdomain or an insecure cookie configuration (cookie tossing), the double-check can be defeated. The more robust variant therefore additionally signs the token with a server-side secret (HMAC), so a smuggled-in cookie without a valid signature stands out immediately. Combined with __Host- cookie prefixes, which technically prevent cross-domain cookie setting, the mechanism becomes significantly more robust.
<?php
declare(strict_types=1);
/**
* Validates a double-submit CSRF token using an HMAC-signed cookie value.
* Compares the signed cookie value against the value sent in the request header.
*/
final class DoubleSubmitCsrfValidator
{
public function __construct(private readonly string $hmacSecret)
{
}
public function isValid(?string $cookieToken, ?string $headerToken): bool
{
if ($cookieToken === null || $headerToken === null) {
return false;
}
// Constant-time comparison prevents timing side-channel attacks
if (!hash_equals($cookieToken, $headerToken)) {
return false;
}
[$rawToken, $signature] = array_pad(explode('.', $cookieToken, 2), 2, '');
$expectedSignature = hash_hmac('sha256', $rawToken, $this->hmacSecret);
return hash_equals($expectedSignature, $signature);
}
}
5. The SameSite cookie attribute as a complementary defense
The SameSite attribute instructs the browser not to send cookies on cross-site requests at all. Strict blocks the cookie completely on any cross-site navigation attempt, even a plain link click from a foreign page, which can make sense for session cookies in security-critical applications but limits the user experience. Lax, today's default in modern browsers, allows the cookie on simple top-level navigations like GET links, but blocks it on POST form submits and on embedded resources such as iframes or images. None disables the restriction entirely and strictly requires the Secure attribute.
SameSite is explicitly not a replacement for token-based CSRF protection, but a complementary layer of defense. Older browsers ignore the attribute entirely and send cookies as usual. Subdomain attacks, where an attacker controls a different subdomain of the same registrable domain, still count as "same-site" under some browser implementations and bypass the protection. And GET-based state changes, which exist against HTTP semantics, still fire under Lax. Defense in depth means SameSite meaningfully reduces the attack surface but never replaces explicit token validation.
# Nginx: enforce Secure and SameSite attributes on the session cookie
# proxied from the application, as a defense-in-depth layer alongside
# explicit CSRF token validation in the application code
proxy_cookie_flags session_id secure samesite=lax;
# PHP: configure session cookie params before session_start()
<?php
declare(strict_types=1);
// Configure the session cookie with SameSite as a complementary defense,
// not as a replacement for synchronizer token validation
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '.example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
6. Why GET requests must never have side effects
The HTTP specification defines GET as a safe method that must not trigger side effects on the server, and as idempotent, meaning it can be repeated any number of times with the same result. Browsers, proxies, crawlers, and prefetching mechanisms rely on this guarantee and automatically fire GET requests without asking, for example when preloading links, reloading a page, or indexing content via search engine bots. Historically well-known CSRF incidents exploited exactly this: a simple <img src="/account/delete?id=42"> on a foreign page was enough to trigger a deletion because the endpoint incorrectly responded to GET.
Consistently restricting state-changing operations to POST, PUT, PATCH, or DELETE removes the basis for CSRF attacks via simple links, images, or prefetching from the outset, because those mechanisms only ever trigger GET. This is not a complete CSRF defense, since POST requests can also be forged via auto-submitting forms, but it is the absolute baseline requirement. REST-compliant API designs that strictly treat GET as read-only rule out an entire class of trivial attacks before token validation even comes into play.
7. Magento's form_key CSRF protection in detail
Magento implements the synchronizer token pattern via the so-called form_key. At session start, Magento\Framework\Data\Form\FormKey generates a random token and stores it in the session. In every form, the value is rendered as a hidden field via $block->getFormKey() or the built-in form_key.phtml template. For classic controllers extending Magento\Framework\App\Action\Action, Magento\Framework\Data\Form\FormKey\Validator automatically checks on POST requests whether the submitted form_key matches the session value, unless the controller explicitly overrides _isAllowed() without this check.
Since Magento 2.3, newer REST and controller endpoints additionally use Magento\Framework\App\CsrfAwareActionInterface, which mandates createCsrfValidationException() and validateForCsrf(). The most common mistake in new controllers: implementing validateForCsrf() with an unreflected return true;, because a developer wanted to "temporarily" disable the check, leaves the endpoint permanently unprotected. Equally often forgotten: custom AJAX controllers that don't extend the standard action class but implement HttpPostActionInterface directly must wire in CSRF validation explicitly themselves, they do not inherit it automatically.
<?php
declare(strict_types=1);
namespace Mironsoft\Example\Controller\Adminhtml\Item;
use Magento\Backend\App\Action;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
/**
* Controller demonstrating explicit CSRF validation for a new admin endpoint.
* Extending Action alone is not sufficient once CsrfAwareActionInterface
* is implemented explicitly; validateForCsrf() must perform a real check.
*/
class Delete extends Action implements HttpPostActionInterface, CsrfAwareActionInterface
{
/**
* Rejects the request with a CSRF exception when validation fails.
*
* @param RequestInterface $request
* @return InvalidRequestException|null
*/
public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
{
return null;
}
/**
* Performs the actual form_key comparison against the session value.
* Returning true unconditionally here disables CSRF protection entirely.
*
* @param RequestInterface $request
* @return bool|null
*/
public function validateForCsrf(RequestInterface $request): ?bool
{
// Delegate to the framework's default form_key validation logic
return null;
}
/**
* @return \Magento\Backend\Model\View\Result\Redirect
*/
public function execute()
{
// Item deletion logic runs only after CSRF validation has passed
return $this->resultRedirectFactory->create()->setPath('*/*/');
}
}
8. CSRF protection for AJAX, SPAs, and API endpoints
Even AJAX requests originating from the same origin carry the session cookie automatically and therefore need the same CSRF protection as classic forms. The common pattern: the server renders the token into a <meta> tag or a dedicated JSON configuration during the initial page build, the frontend reads it and sends it on every state-changing request as a custom header, for example X-CSRF-Token, instead of in the body. The decisive advantage of custom headers over body parameters: custom headers simply cannot be set from plain HTML without JavaScript, so a simple HTML form on an attacker's page is structurally unable to forge them.
For single-page applications, a dedicated token endpoint is recommended, called at app startup or after login and returning the current token, combined with rotation after every sensitive operation. Pure API endpoints for third-party systems that authenticate via API keys or OAuth bearer tokens instead of cookies generally don't need CSRF protection, because the browser never automatically attaches bearer tokens from Authorization headers, which is exactly the difference from cookie-based authentication that makes CSRF possible in the first place.
// Read the CSRF token from a meta tag rendered by the server
// and send it as a custom header on every state-changing fetch request
function getCsrfToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : null;
}
async function updateAccountEmail(email) {
const response = await fetch('/api/account/email', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(),
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
9. CSRF defense mechanisms compared
No single measure fully covers every CSRF attack vector. Only the interplay of token validation, clean HTTP method semantics, and cookie attributes produces a resilient defense. The table below contrasts insecure default assumptions with the recommended, secure approaches.
| Aspect | Insecure | Secure approach | Rationale |
|---|---|---|---|
| Checking authentication | Valid session cookie alone as proof | Session cookie + synchronizer token | A cookie alone doesn't confirm a deliberate user action |
| Triggering a state change | Via GET links like /delete?id=42 | Exclusively via POST/PUT/DELETE | GET is fired automatically by browsers and bots |
| Token comparison | Simple == string comparison |
hash_equals(), constant time |
Prevents timing-based side-channel attacks |
| Cookie configuration | SameSite=None with no further check | SameSite=Lax/Strict + token validation | SameSite complements token protection, doesn't replace it |
| AJAX token transport | Token only in the request body, like a normal form field | Custom header like X-CSRF-Token |
Custom headers cannot be set without JavaScript |
| Magento controller | validateForCsrf() hard-returns true |
Real form_key check via FormKey\Validator |
A hardcoded true permanently disables protection |
Mironsoft
Web application security, code audits, and Magento hardening
Ready to close your CSRF gaps for good?
We audit your forms, AJAX endpoints, and Magento controllers for missing or broken CSRF validation, and implement token patterns, SameSite configuration, and form_key hardening properly.
CSRF audit
Systematic review of every state-changing endpoint and controller
Magento hardening
Retrofitting correct form_key and CsrfAwareActionInterface implementations
API hardening
Introducing token header patterns for AJAX, SPAs, and REST endpoints
10. Summary
Implementing CSRF protection correctly means accepting the browser's automatic cookie inclusion as a given and never mistaking it for proof of a deliberate user action. The synchronizer token pattern remains the most reliable method because it requires a value that an attacker fundamentally cannot guess or read. Double-submit cookies offer a stateless alternative for distributed systems, as long as they are additionally signed. SameSite meaningfully reduces the attack surface but never replaces explicit validation in application code.
Just as important as the token mechanics is a strict separation between read and write HTTP methods: GET must never trigger side effects. In Magento, the form_key handles this automatically for classic controllers, but every new custom controller and every AJAX endpoint must implement validation explicitly and correctly, rather than accidentally disabling it with a hardcoded true.
Implementing CSRF Protection Correctly, the Essentials at a Glance
Synchronizer token
Server generates an unpredictable per-session token, validated via hash_equals(), never with ==.
SameSite as a complement
Lax or Strict reduces the attack surface but never replaces token validation in code.
GET stays read-only
State changes exclusively via POST/PUT/DELETE, never triggerable via GET links or image tags.
Magento form_key
FormKey\Validator for standard controllers, correctly implement CsrfAwareActionInterface for custom endpoints.