← All posts
CSRFsecuritySaaSvibe codingprevent CSRF attack

How to Prevent CSRF Attacks in SaaS Apps: Complete Guide

O

OverMCP Team

TL;DR: How to Prevent CSRF Attacks in a SaaS App

To prevent CSRF attacks in your SaaS app, implement CSRF tokens for all state-changing requests, set cookies with SameSite=Strict or Lax, and validate the Origin or Referer header on your server. For APIs, require a custom header like X-Requested-By. These three layers stop attackers from tricking authenticated users into performing unintended actions.

What Is a CSRF Attack and Why Should SaaS Devs Care?

Cross-Site Request Forgery (CSRF) is an attack where a malicious website, email, or blog causes a user's browser to make an unwanted request to your SaaS app. If the user is authenticated, the attacker can change settings, delete data, or initiate financial transactions without consent.

For vibe-coded SaaS apps built with AI tools, CSRF is especially dangerous because:

  • AI-generated code often skips CSRF protection
  • Many modern frameworks (Next.js, Express, Django) have built-in CSRF defenses that devs forget to enable
  • Single-page apps (SPAs) and APIs are commonly misconfigured
  • Let's walk through exactly how to prevent CSRF attacks in your app.

    How CSRF Attacks Work: The Core Mechanism

    A CSRF attack exploits the fact that browsers automatically include cookies (including session cookies) in requests to the domain they belong to. Here's a typical flow:

  • User logs into your SaaS app (example.com). A session cookie is set.
  • User visits a malicious site (evil.com) without logging out.
  • evil.com contains an image or form that submits a request to example.com/change-email.
  • The browser automatically includes the session cookie, so the server thinks the request is legitimate.
  • The email is changed, and the attacker can now reset the password.
  • Step 1: Implement CSRF Tokens (The Gold Standard)

    A CSRF token is a unique, unpredictable value generated by the server and included in every state-changing request (POST, PUT, DELETE). The server validates the token before processing the request.

    Server-Side Token Generation (Node.js/Express Example)

    // Using csurf middleware (or implement your own)
    import csrf from 'csurf';
    const csrfProtection = csrf({ cookie: true });
    
    app.use(csrfProtection);
    
    app.get('/form', (req, res) => {
      res.render('form', { csrfToken: req.csrfToken() });
    });
    
    app.post('/process', (req, res) => {
      // CSRF token automatically validated
      res.send('Data processed');
    });

    Including the Token in HTML Forms

    <form action="/change-email" method="POST">
      <input type="hidden" name="_csrf" value="<%= csrfToken %>">
      <input type="email" name="newEmail">
      <button type="submit">Change Email</button>
    </form>

    For SPAs or APIs: Custom Header Token

    // Frontend (React/Vue)
    const response = await fetch('/api/change-email', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
      },
      body: JSON.stringify({ newEmail })
    });
    
    // Backend validation
    app.use((req, res, next) => {
      const token = req.headers['x-csrf-token'];
      if (!isValidToken(token)) {
        return res.status(403).json({ error: 'Invalid CSRF token' });
      }
      next();
    });

    Step 2: Set SameSite Cookie Attribute

    The SameSite cookie attribute tells the browser when to send cookies. This is a powerful defense that works even if CSRF tokens are forgotten.

    // Setting session cookie with SameSite=Strict
    res.cookie('sessionId', sessionId, {
      httpOnly: true,
      secure: true,
      sameSite: 'Strict' // or 'Lax'
    });
  • Strict: Cookie is never sent for cross-site requests. Best for security but may break some legitimate navigation (e.g., clicking a link from an email).
  • Lax: Cookie is sent for top-level navigation (GET requests) but not for POST forms or AJAX. Good balance for most SaaS apps.
  • None: Cookie is sent for all requests (requires Secure flag). Only use if you need cross-site requests.
  • Step 3: Validate Origin/Referer Headers

    Check the Origin or Referer header to ensure the request comes from your own domain. This is a simple fallback.

    app.use('/api', (req, res, next) => {
      const origin = req.headers['origin'];
      const referer = req.headers['referer'];
      const allowedOrigins = ['https://yourapp.com'];
      
      if (origin && !allowedOrigins.includes(origin)) {
        return res.status(403).json({ error: 'Invalid origin' });
      }
      
      // Or check referer
      if (referer && !referer.startsWith('https://yourapp.com')) {
        return res.status(403).json({ error: 'Invalid referer' });
      }
      
      next();
    });

    Step 4: Disable CORS for Untrusted Origins

    CORS settings can inadvertently allow CSRF. Be explicit about which origins can access your API.

    app.use(cors({
      origin: 'https://yourapp.com', // Only allow your frontend
      credentials: true
    }));

    Never use a wildcard * with credentials.

    Common Pitfalls to Avoid

  • Not using CSRF tokens for GET requests – GET requests should be side-effect-free, but if they aren't, protect them too.
  • Storing CSRF tokens in localStorage – Tokens should be stored server-side or in a cookie that's not accessible to JavaScript (httpOnly).
  • Using predictable tokens – Always use a cryptographically secure random generator.
  • Not rotating tokens – Generate a new token after a user logs in or after a sensitive action.
  • Forgetting to protect API endpoints – Both web forms and API endpoints need CSRF protection.
  • Framework-Specific Defenses

    Next.js (Pages Router)

    Next.js has built-in CSRF protection via getServerSideProps and middleware. For API routes, use the csrf utility from next-csrf.

    // pages/api/change-email.js
    import { csrf } from 'next-csrf';
    
    const csrfMiddleware = csrf();
    
    export default async function handler(req, res) {
      await csrfMiddleware(req, res);
      // Your handler
    }

    Django

    Django includes CSRF middleware by default. Ensure it's enabled in MIDDLEWARE:

    # settings.py
    MIDDLEWARE = [
        'django.middleware.csrf.CsrfViewMiddleware',
        # ...
    ]

    And include {% csrf_token %} in all forms.

    Express.js

    Use the csurf middleware (or its successor csrf-csrf). Always pass the token to views.

    Why Vibe-Coded Apps Are Vulnerable

    AI coding tools often generate code that:

  • Omits CSRF token generation
  • Uses SameSite=None for third-party integrations
  • Exposes API endpoints without authentication checks
  • Sets Access-Control-Allow-Origin: *
  • If you built your SaaS app with AI, you need to audit these areas. Tools like OverMCP can scan your codebase for missing CSRF protections and other security gaps, giving you a clear fix list.

    FAQ

    What is the most effective way to prevent CSRF attacks?

    The most effective way is to use CSRF tokens combined with the SameSite cookie attribute. Tokens provide server-side validation, while SameSite prevents browsers from sending cookies cross-origin. Together, they block nearly all CSRF vectors.

    Can CSRF happen over HTTPS?

    Yes. HTTPS only encrypts data in transit; it doesn't prevent CSRF. The attack exploits browser cookie behavior, not network eavesdropping. Always use CSRF protection regardless of HTTPS.

    Do I need CSRF protection for a REST API used by a mobile app?

    No, if your API is only consumed by a mobile app, CSRF tokens aren't needed because mobile apps don't automatically include cookies. However, if a web frontend also uses the same API, you need CSRF protection for that web client.

    Is your app secure?

    Free scan in 30 seconds. No signup needed.

    Scan My App Free