why a single endpoint needs different rules
Many REST teams rely on simple content-type checks or the assumption that cross-site requests only ever touch GET requests. GraphQL bundles queries and mutations behind a single POST endpoint, rendering these assumptions worthless. Anyone running a GraphQL API with cookie-based sessions needs explicit CSRF protection that accounts for the specifics of that single endpoint.
Table of Contents
- 1. What CSRF is and when it becomes relevant at all
- 2. Why GraphQL breaks the assumptions REST relies on
- 3. A concrete CSRF attack against a GraphQL mutation
- 4. Content-type checking as the first line of defense
- 5. Configuring SameSite cookies correctly
- 6. CSRF tokens for GraphQL mutations
- 7. Why bearer token auth sidesteps CSRF structurally
- 8. CSRF protection for Magento GraphQL in context
- 9. Protection measures compared
- 10. Summary
- 11. FAQ
1. What CSRF is and when it becomes relevant at all
Cross-Site Request Forgery, CSRF for short, exploits the fact that browsers automatically attach cookies to every request to the matching domain, regardless of which website triggered the request. If a logged-in user visits a malicious page, that page can trigger a request to a different domain in the background, for instance through an auto-submitted form, and the browser automatically attaches the valid session cookies. What matters for CSRF protection in GraphQL: the risk exists exclusively with cookie-based authentication, not with auth headers.
If a GraphQL API runs exclusively on bearer tokens in the authorization header, the classic CSRF risk disappears structurally, because an attacker cannot set that header from a foreign domain without bypassing the same-origin policy. But once session cookies come into play, for example server-rendered storefronts with a classic PHP session, or GraphQL APIs that use cookie auth for convenience, CSRF protection for GraphQL becomes a mandatory task.
2. Why GraphQL breaks the assumptions REST relies on
Classic REST CSRF protection often relies on two assumptions: first, that state-changing operations run over POST, PUT, PATCH or DELETE, while GET requests are safe because they should be idempotent. Second, that different endpoints have different URLs, so a targeted attack has to aim at a specific action. Neither assumption holds for GraphQL, because CSRF protection for GraphQL must account for the fact that practically every operation, queries as well as mutations, runs through the same single POST endpoint.
That means an attacker no longer needs to know different URLs for different actions, just the one GraphQL endpoint and the query or mutation structure they want to execute. Some GraphQL servers additionally accept GET requests with the query as a URL parameter, which completely undermines the REST principle "GET is safe" if the same GET route also allows mutations, or if an otherwise harmless query exposes sensitive data. CSRF protection for GraphQL therefore has to be approached fundamentally differently from REST.
3. A concrete CSRF attack against a GraphQL mutation
A classic attack vector without CSRF protection for GraphQL exploits the fact that HTML forms can trigger cross-origin POST requests with the content type application/x-www-form-urlencoded, entirely without JavaScript and without a CORS preflight, since this content type counts as a "simple request". If the GraphQL server also accepts query strings under this content type instead of strictly requiring application/json, an attacker can trigger a mutation such as an address change or a payment method update through an invisible, auto-submitted form.
<!-- Malicious page hosted on attacker.example -->
<!-- Auto-submits a cross-site POST that rides the victim's session cookie -->
<form id="csrf-form" action="https://shop.mironsoft.de/graphql" method="POST"
enctype="application/x-www-form-urlencoded">
<input type="hidden" name="query"
value="mutation { updateCustomerEmail(email: "attacker@evil.example") { id } }">
</form>
<script>document.getElementById('csrf-form').submit();</script>
Without a content-type check and without a CSRF token, the server processes this request, because the valid session cookie is attached automatically. The user notices nothing, while a sensitive change is made to their account in the background. This example shows why CSRF protection for GraphQL is not optional once cookie sessions are involved.
4. Content-type checking as the first line of defense
The simplest and most effective measure for CSRF protection in GraphQL is to accept requests exclusively with the content type application/json and strictly reject all others. Since application/json is not among the "simple request" content types of the fetch specification, the browser automatically forces a CORS preflight request for cross-origin requests with this content type. The server can use this preflight to reject the request before the actual request ever reaches the server.
This measure alone already blocks the vast majority of naive CSRF attacks via HTML forms, since forms can only send application/x-www-form-urlencoded, multipart/form-data or text/plain as content type, never application/json. Apollo Server offers a built-in csrfPrevention option for exactly this content-type check, additionally requiring a custom header like Apollo-Require-Preflight to also catch requests with a manipulated content-type header.
// server.js — Apollo Server's built-in CSRF prevention
import { ApolloServer } from '@apollo/server';
const server = new ApolloServer({
schema,
// Rejects requests without a proper JSON content-type or a required
// non-simple header, forcing a CORS preflight for cross-origin calls
csrfPrevention: true,
});
// client.ts — clients must now send a non-simple header explicitly
fetch('https://api.mironsoft.de/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Apollo-Require-Preflight': 'true',
},
credentials: 'include',
body: JSON.stringify({ query: '{ me { id } }' }),
});
5. Configuring SameSite cookies correctly
The cookie attribute SameSite is a second, independent line of defense for CSRF protection in GraphQL. With SameSite=Lax, the default in modern browsers, cookies are not sent along with most cross-site requests, with the exception of top-level navigations via GET. That is enough to block auto-submitted cross-site POST forms without impairing legitimate navigation between pages. For maximum security, SameSite=Strict can be set, but that also breaks legitimate links from external sites into your own logged-in session.
Important for CSRF protection in GraphQL in multi-domain setups, for example when the frontend and the GraphQL API run on different subdomains: SameSite=Lax already does not count as cross-site between different subdomains of the same registrable domain, as long as the cookie domain is correctly set to the parent domain. If the API runs on a completely separate domain, however, say for headless setups with separate hosting, you additionally need the Secure attribute and explicit CORS configuration, so SameSite protection and cross-origin access work together cleanly.
// cookie-config.js — setting the session cookie with SameSite and Secure
res.cookie('PHPSESSID', sessionId, {
httpOnly: true,
secure: true, // only sent over HTTPS
sameSite: 'lax', // blocks cross-site POST form submissions
domain: '.mironsoft.de', // shared across subdomains, not cross-site
maxAge: 1000 * 60 * 60 * 2,
});
// Nginx equivalent when cookies are set at the proxy layer
// add_header Set-Cookie "PHPSESSID=$upstream_cookie; SameSite=Lax; Secure; HttpOnly";
6. CSRF tokens for GraphQL mutations
For environments where content-type checks and SameSite cookies alone are not enough, say older browsers or particularly sensitive mutations like payment changes, the classic CSRF token pattern remains a robust addition. The server generates a random token when the page is delivered, landing both in a cookie and in a hidden form field or a JavaScript-accessible meta tag. Every GraphQL mutation must send this token as an additional header, one an attacker cannot read from a foreign domain, since same-origin policy prevents access to the cookie or the DOM of the target page.
The implementation for GraphQL barely differs from REST, with one exception: since all mutations run through the same endpoint, a single central middleware checking the token on every incoming POST request is enough, instead of duplicating the check per route. This centralization is one of the few advantages the single GraphQL endpoint offers for CSRF protection in GraphQL over many distributed REST routes.
// csrf-middleware.js — one central check for the single GraphQL endpoint
function csrfTokenMiddleware(req, res, next) {
// Only mutations change state, but checking every POST keeps this simple
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({
errors: [{ message: 'Invalid or missing CSRF token', extensions: { code: 'CSRF_TOKEN_INVALID' } }],
});
}
next();
}
app.post('/graphql', csrfTokenMiddleware, graphqlHandler);
7. Why bearer token auth sidesteps CSRF structurally
The most effective, if architecturally the most far-reaching, protection against CSRF is dropping cookie-based authentication entirely in favor of bearer tokens in the authorization header. Since browsers do not automatically send headers cross-origin the way they do cookies, an attacker cannot forge the authorization header without direct access to the token. For new GraphQL APIs without legacy cookie requirements, bearer token auth is therefore often the simpler solution for CSRF protection in GraphQL, because the entire problem disappears structurally instead of being contained with multiple layers of protection.
The trade-off: bearer tokens must be stored client-side, usually in the browser's local storage, which in turn opens a different risk, namely cross-site scripting, which can read the token directly. For Magento storefronts that rely on session cookies for the cart anyway, a pure switch to bearer tokens is rarely practical, which is why content-type checks, SameSite cookies and CSRF tokens remain the more realistic combination there.
8. CSRF protection for Magento GraphQL in context
Magento's native GraphQL module frequently uses a combination of a bearer token for the customer API and a session cookie for cart context for logged-in storefront customers, which makes CSRF protection in GraphQL a special challenge in Magento environments, since both auth mechanisms can be active simultaneously. Mutations that touch the quote cookie, such as addProductsToCart, are potentially CSRF-vulnerable if the endpoint does not enforce a content-type check, even if pure customer authentication runs over a bearer token.
In practice, Magento GraphQL setups benefit from implementing a custom middleware or plugin on the GraphQL controller that strictly checks the content type against application/json and additionally controls the SameSite attribute of the cart cookie, instead of relying on Magento's default configuration, which is primarily designed for classic form-based CSRF attacks in the admin area, not for the GraphQL endpoint itself.
<?php
declare(strict_types=1);
namespace Mironsoft\GraphQlSecurity\Plugin;
use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\Webapi\Exception as WebapiException;
/**
* Enforces a strict JSON content-type check on the GraphQL controller
* to prevent form-based CSRF attacks against cart and customer mutations.
*/
class CsrfContentTypeGuardPlugin
{
/**
* Rejects any GraphQL request that is not sent as application/json.
*
* @param \Magento\GraphQl\Controller\GraphQl $subject
* @param HttpRequest $request
* @return void
* @throws WebapiException
*/
public function beforeDispatch(
\Magento\GraphQl\Controller\GraphQl $subject,
HttpRequest $request
): void {
$contentType = (string) $request->getHeader('Content-Type');
if (!str_starts_with($contentType, 'application/json')) {
throw new WebapiException(__('Invalid content type for GraphQL request'), 0, 403);
}
}
}
9. Protection measures compared
The table below compares the presented measures for CSRF protection in GraphQL by effectiveness and implementation effort.
| Measure | Effectiveness | Effort | Side effects |
|---|---|---|---|
| Content-type check | High | Very low | None with correct clients |
| SameSite=Lax cookies | High | Low | Minimal, good modern browser support |
| CSRF token | Very high | Medium | Additional client-side logic needed |
| Bearer token instead of cookies | Structurally immune | High (architecture change) | XSS risk from token storage |
For most production GraphQL APIs with cookie sessions, the combination of content-type checking and SameSite cookies is the pragmatic baseline protection, complemented by explicit CSRF tokens for particularly sensitive mutations like payment or email changes.
Mironsoft
GraphQL security, Magento integrations and API hardening
Is your GraphQL API actually protected against CSRF?
We audit your GraphQL endpoint for content-type validation, cookie configuration and CSRF token handling, specifically in interplay with Magento cart sessions.
Security audit
Auditing the GraphQL endpoint for CSRF attack vectors and cookie configuration
Hardening
Setting up content-type checks, SameSite cookies and CSRF tokens in production
Magento integration
CSRF protection specifically for cart and customer mutations in Magento GraphQL
10. Summary
CSRF protection for GraphQL endpoints differs fundamentally from classic REST protection, because a single POST endpoint bundles every query and mutation, and the usual REST assumptions about safe GET requests and separate routes no longer hold. Once an API works with cookie-based sessions, explicit hardening is mandatory, not optional, since attackers can otherwise trigger state-changing mutations through auto-submitted forms without the user noticing anything.
The most robust combination consists of strict content-type checking that only accepts application/json, correctly configured SameSite cookies, and CSRF tokens for particularly sensitive mutations. Anyone who can switch entirely to bearer token authentication sidesteps the CSRF problem structurally, but then has to secure the risk of client-side stored tokens against XSS. For Magento GraphQL setups with cart cookies, a custom middleware on the GraphQL controller remains the most reliable path to complete CSRF protection for GraphQL.
CSRF Protection for GraphQL Endpoints — Key Takeaways
One endpoint, new risks
Every operation runs through one URL, classic REST assumptions about safe GET requests no longer hold.
Content-type as baseline protection
Accepting only application/json forces a CORS preflight and blocks naive form-based attacks.
SameSite and CSRF tokens
SameSite=Lax as a second layer, CSRF tokens for particularly sensitive mutations like payment changes.
Bearer tokens as a structural fix
No cookie, no CSRF risk, but requires XSS hardening for client-side stored tokens.