CSRF Protection for GraphQL Endpoints: What Differs From REST
AI generated
{ }
type
GraphQL · CSRF · Security · Cookies
CSRF Protection for GraphQL Endpoints
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.

16 min read Content-Type Checks · SameSite · CSRF Token Apollo Server · Magento GraphQL · Cookies

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.

11. FAQ: CSRF Protection for GraphQL Endpoints

1When is CSRF relevant for GraphQL?
Only with cookie-based auth, pure bearer tokens eliminate the risk structurally.
2Why does it differ from REST?
A single POST endpoint for every operation invalidates REST assumptions about safe GET requests.
3How does content-type checking protect?
Forms cannot send application/json, a CORS preflight blocks unauthorized cross-origin requests.
4What does csrfPrevention do in Apollo Server?
Checks content type and requires an additional header that cannot be set without a preflight.
5Is SameSite=Lax enough alone?
Mostly yes against form attacks, but better combined with content-type checking.
6When do you need CSRF tokens?
For sensitive mutations like payment changes, or older browsers without SameSite support.
7Does bearer auth solve CSRF fully?
Structurally yes, but creates a new XSS risk for client-side token storage.
8Is Magento GraphQL protected by default?
Not fully, a custom middleware for the GraphQL controller is recommended.
9Are pure queries CSRF-vulnerable?
Not directly, indirectly relevant with GET routes allowing mutations or exposing sensitive data.
10Needed for pure headless APIs?
No, without session cookies the classic CSRF risk disappears entirely.