Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Preventing JSON CSRF Attacks

Cross-Site Request Forgery (CSRF) is a type of malicious exploit where unauthorized commands are transmitted from a user that the web application trusts. While often associated with traditional HTML form submissions that trigger state-changing actions via GET or POST requests with URL-encoded or multipart bodies, APIs that accept JSON payloads are also vulnerable, especially if not properly designed and secured. This page explores the nature of JSON CSRF and how to defend against it.

Understanding JSON CSRF

A JSON CSRF attack occurs when a malicious website tricks a user's browser into making an unintended request to a web application where the user is currently authenticated. If the application's API endpoint accepts a JSON payload and relies solely on session cookies (which browsers automatically attach to requests for the target domain), the malicious request can be indistinguishable from a legitimate one from the server's perspective, leading to unauthorized actions.

Traditionally, CSRF attacks leveraged HTML <form> elements or image tags (<img>) to trigger simple GET/POST requests with limited content types (application/x-www-form-urlencoded or multipart/form-data). However, modern APIs frequently use JSON (application/json), which cannot be sent via simple GET/POST HTML forms without JavaScript. This requires the attacker to use JavaScript on the malicious page.

How a JSON CSRF Attack Might Work:

  1. A user is logged into an application (e.g., bank.com). Their browser has a session cookie for bank.com.
  2. The user visits a malicious website (e.g., attacker.com).
  3. The malicious website contains JavaScript that creates and submits a request to a sensitive endpoint on bank.com (e.g., POST /api/transfer-funds) usingXMLHttpRequest or Fetch API.
  4. The JavaScript crafts a request with a JSON body, for example:
    fetch('https://bank.com/api/transfer-funds', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // Browser automatically attaches cookies for bank.com
      },
      body: JSON.stringify({
        recipient: 'attacker_account',
        amount: 1000
      })
    });
  5. The browser automatically includes the session cookie for bank.com with this request.
  6. If bank.com relies only on the cookie for authentication and doesn't validate the request source or origin, it will process the request as if it came from the legitimate user.

Prevention Strategies

Defending against JSON CSRF requires measures beyond just relying on cookies. Here are several strategies, often used in combination, to protect your endpoints:

1. Check the Content-Type Header

While older CSRF attacks were limited to simple content types, JavaScript-based requests can set arbitrary headers, including Content-Type: application/json. However, many web frameworks and server-side libraries by default only parse the request body as JSON if this specific header is present.

Requests triggered by simple HTML forms (without JavaScript) typically sendapplication/x-www-form-urlencoded or multipart/form-data. A server configured to only accept JSON payloads when the Content-Type is explicitly application/json can reject form-based CSRF attempts. This doesn't stop JavaScript-based JSON CSRF, but it's a basic step.

Note: Some older or non-standard browser behaviors might still allow sendingapplication/json with simpler methods, but this is less common now.

2. Check the Origin or Referer Header

Modern browsers include the Origin header in cross-origin requests (including those made via Fetch or XMLHttpRequest). This header indicates the origin (scheme, hostname, port) of the page that initiated the request. Similarly, the Referer header (though can be controlled or suppressed by the user/browser settings) might also contain the origin.

Your server can check these headers to ensure the request originated from your legitimate domain(s). If the Origin or Referer does not match your expected origin, the request should be rejected.

Conceptual Server-Side Check:

// Example in a Node.js/Express-like framework
function checkOrigin(req, res, next) {
  const origin = req.headers['origin']; // Or req.headers['referer']
  const allowedOrigins = ['https://your-app.com', 'https://subdomain.your-app.com']; // Configure your allowed origins

  if (!origin || allowedOrigins.includes(origin)) {
    // Request is from a trusted origin or a same-origin request (origin might be null/undefined)
    next();
  } else {
    // Request is from an untrusted origin
    res.status(403).send('CSRF detected: Invalid Origin header');
  }
}

Caution: The Referer header is not always reliable due to privacy settings or proxies. The Origin header is generally preferred for this purpose for modern browsers.

3. Use CSRF Tokens (Synchronizer Token Pattern)

This is a widely used and effective defense. It involves:

  • When a user requests a page (e.g., a form or a page making API calls), the server embeds a unique, randomly generated token within the page content.
  • This same token is also stored server-side, typically in the user's session.
  • When the user's browser makes a state-changing request (e.g., a POST, PUT, DELETE request), the token is sent back to the server, either in a hidden form field (for HTML forms), a custom HTTP header (common for AJAX/JSON requests), or as part of the request body/query parameters.
  • The server verifies that the token received in the request matches the token stored in the user's session. If they don't match, the request is rejected.

For JSON APIs consumed by frontends using Fetch or XMLHttpRequest, sending the token in a custom HTTP header (like X-CSRF-Token or X-Requested-With) is a standard approach. Browsers enforce the Same-Origin Policy for custom headers, meaning malicious cross-origin JavaScript cannot add arbitrary custom headers to a request without a preflight CORS request (which the server can then deny).

Conceptual Frontend (JavaScript) and Server-Side Snippets:

Frontend (after obtaining the token, e.g., from a meta tag or initial API call):

const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

fetch('/api/sensitive-action', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken, // Send token in custom header
  },
  body: JSON.stringify({ data: '...' })
});

Server-Side (Verification):

// Example in a Node.js/Express-like framework
function verifyCsrfToken(req, res, next) {
  const clientToken = req.headers['x-csrf-token']; // Get token from header
  const serverToken = req.session.csrfToken; // Get token from session

  if (clientToken && serverToken && clientToken === serverToken) {
    // Token is valid
    next();
  } else {
    // Invalid or missing token
    res.status(403).send('CSRF token verification failed');
  }
}

// In your route definition:
// app.post('/api/sensitive-action', verifyCsrfToken, handleSensitiveAction);

4. Use SameSite Cookies

The SameSite attribute on cookies is a powerful defense. It tells the browser whether to send cookies with cross-site requests.

  • SameSite=Lax (default in modern browsers): Cookies are sent with top-level navigations (like clicking a link) but not with other cross-site requests (like AJAX calls, images, or iframes) unless it's a GET request for top-level navigation. This significantly mitigates CSRF for state-changing methods (POST, PUT, DELETE).
  • SameSite=Strict: Cookies are only sent with same-site requests (when the site in the address bar matches the site of the cookie). This provides the strongest protection but can break legitimate cross-site links (e.g., linking from an external site).
  • SameSite=None; Secure: Cookies are sent with all requests, including cross-site ones. Requires the Secure attribute (HTTPS). Use this only for specific cross-site needs (e.g., third-party cookies, embedded content that needs authentication) and ensure *other* CSRF defenses are in place.

Setting your session cookies with at least SameSite=Lax dramatically reduces the risk of CSRF, as the session cookie needed for authentication won't be sent with typical cross-site AJAX/Fetch requests initiated by an attacker's script.

5. Defend Against JSON Array/Object Literal Execution (Older Vulnerability)

In older browsers or specific contexts, if a JSON API endpoint returned sensitive data as a top-level array ([...]) or object ({...}), a malicious page could potentially include the API endpoint URL as a <script> tag'ssrc. The browser might execute the response as JavaScript.

If the sensitive data was returned as a simple JSON array (e.g., [{...}, {...}]), this response was also a valid JavaScript array literal. In some scenarios (especially pre-ES5 browsers or specific execution contexts like overriding Array constructors), the malicious page could potentially read the values of this array. Similarly, if it was a simple object literal ({...}), it could potentially be assigned to a variable if the response was wrapped in parentheses.

To mitigate this, sensitive JSON API responses should *always* be wrapped in a top-level object that is not a valid JavaScript literal (e.g., { "data": [...] } or prefixing the response with an unexecutable string like while(1);{ ... }). Modern browsers are less susceptible to this specific vector, but it's a good practice for defense in depth, especially if supporting older clients.

6. Use Appropriate HTTP Methods

Ensure that state-changing operations (like creating, updating, deleting resources) are handled by HTTP methods other than GET (e.g., POST, PUT, DELETE). GET requests should ideally be idempotent and not have side effects. While this doesn't *prevent* JavaScript-based CSRF using other methods, it prevents CSRF via simple GET requests triggered by <img> or link clicks.

Defense in Depth

No single CSRF prevention technique is foolproof in isolation across all possible scenarios and browser behaviors (especially historical ones). The most robust defense involves combining multiple layers:

  • Use SameSite=Lax (or Strict) for session cookies.
  • Implement CSRF tokens for all state-changing API requests.
  • Validate the Origin header for sensitive endpoints.
  • Ensure sensitive data responses are not parseable as executable JavaScript if fetched via <script> tag (wrap in object, prefix).
  • Use appropriate HTTP methods (POST/PUT/DELETE for state changes).

By implementing a combination of these measures, you significantly raise the bar for attackers attempting JSON CSRF, making your application much more secure.

Conclusion

JSON APIs are a common target for CSRF attacks when authentication relies solely on cookies and no other defenses are in place. Developers must be proactive in designing their APIs to include CSRF protection. Leveraging features like SameSite cookies, validating request origins, and implementing CSRF tokens are essential steps to safeguard user data and application integrity against this prevalent web vulnerability.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool