CSRF

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 5 min

Cross-Site Request Forgery (CSRF or XSRF) is an attack that forces an authenticated user to execute unwanted actions on a web application where they are currently authenticated.

Overview

If you use Cookies to store session IDs or JWTs, the browser will automatically attach those cookies to every HTTP request sent to your API domain.

The Attack Scenario:

  1. Alice logs into bank.com. Her browser saves a session cookie for bank.com.
  2. Alice visits evil.com.
  3. evil.com contains a hidden HTML form: <form action="https://bank.com/transfer" method="POST"><input type="hidden" name="to" value="attacker"><input type="hidden" name="amount" value="1000"></form>
  4. JavaScript on evil.com auto-submits the form.
  5. The browser sends the POST request to bank.com and automatically includes Alice’s session cookie.
  6. bank.com sees a valid cookie and processes the transfer!

CSRF protections ensure that state-changing requests (POST, PUT, DELETE) intentionally originated from your frontend, not a malicious third-party site.

Key Concepts

  • CSRF Token: A unique, unpredictable cryptographic token generated by the server and sent to the client. The client must include this token in the body or headers of every POST request. evil.com cannot read this token, so it cannot construct a valid request.
  • SameSite Cookies: A modern browser feature that tells the browser whether or not to include cookies on cross-origin requests.

Code Examples

1. Modern Defense: SameSite Cookies

If your frontend and backend are on the same domain (e.g., app.mycompany.com and api.mycompany.com), the absolute easiest and most effective way to prevent CSRF is to set the SameSite attribute on your authentication cookies to Lax or Strict.

// auth.controller.ts (Example using Express Response)
@Post('login')
login(@Res({ passthrough: true }) res: Response) {
  const jwt = this.authService.generateToken();
  
  res.cookie('access_token', jwt, {
    httpOnly: true,
    secure: true,      // Require HTTPS
    sameSite: 'strict' // The browser will NOT send this cookie if the request originates from evil.com!
  });
  
  return { message: 'Logged in' };
}

Note: If you use SameSite: strict, CSRF is effectively neutralized without needing complex CSRF tokens.

2. Traditional Defense: CSRF Tokens (csurf middleware)

If you cannot use SameSite cookies (e.g., your frontend and backend have completely different domains, or you support very old browsers), you must implement the Double Submit Cookie or CSRF Token pattern.

In Express-based NestJS apps, we use the csurf middleware.

npm install csurf cookie-parser
npm install @types/csurf -D
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import * as csurf from 'csurf';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 1. You must parse cookies first
  app.use(cookieParser());
  
  // 2. Enable CSRF protection (stores the secret in a cookie)
  app.use(csurf({ cookie: true }));

  // 3. Provide an endpoint for the frontend to fetch the token
  // The frontend must call this on startup, and then attach the token 
  // to an 'X-CSRF-Token' header on all subsequent POST requests.
  app.use((req, res, next) => {
    // Attach the token to the response locals (or expose it via a specific GET endpoint)
    res.locals.csrfToken = req.csrfToken();
    next();
  });

  await app.listen(3000);
}
bootstrap();

Best Practices

  • APIs using Authorization Headers are Immune: If your frontend stores a JWT in memory or LocalStorage and attaches it manually via the Authorization: Bearer <token> header, you are completely immune to CSRF. CSRF only works because browsers automatically attach Cookies. If you don’t use authentication cookies, you don’t need CSRF tokens!
  • Never mutate state on GET requests: CSRF protections usually ignore GET requests (they only check tokens for POST/PUT/DELETE). If you have an endpoint like GET /deleteUser?id=123, a simple image tag on evil.com (<img src="https://api.com/deleteUser?id=123">) will successfully delete the user! GET requests must always be purely informational.