Skip to content

CSRF Mitigation Strategies and Best Practices

Introduction to CSRF Mitigation

Cross-Site Request Forgery (CSRF) attacks can be prevented through multiple layers of defense. The most effective approach combines several techniques to create defense-in-depth. This guide covers comprehensive mitigation strategies, from basic implementations to advanced protection mechanisms.

Primary Defense Mechanisms

1. CSRF Tokens (Synchronizer Token Pattern)

How It Works

CSRF tokens are unique, cryptographically secure values that are: - Generated server-side for each user session - Included in legitimate forms and requests - Validated on the server before processing state-changing operations

Implementation Examples

PHP Implementation:

<?php
session_start();

// Generate CSRF token
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

$csrf_token = $_SESSION['csrf_token'];
?>

<!-- In HTML form -->
<form method="POST" action="/transfer">
    <input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>">
    <input type="text" name="amount">
    <input type="text" name="to_account">
    <button type="submit">Transfer</button>
</form>

<!-- Server-side validation -->
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
        die('CSRF token validation failed');
    }

    // Process the request
    transfer_money($_POST['amount'], $_POST['to_account']);
}
?>

Node.js/Express with csurf:

const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

const app = express();

app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));

// CSRF protection middleware
const csrfProtection = csrf({ cookie: true });

// Apply to all POST routes
app.use('/api', csrfProtection);

// In route handler
app.post('/transfer', (req, res) => {
    // CSRF token is automatically validated
    const { amount, toAccount } = req.body;
    transferMoney(amount, toAccount);
    res.json({ success: true });
});

// Get CSRF token for forms
app.get('/csrf-token', csrfProtection, (req, res) => {
    res.json({ csrfToken: req.csrfToken() });
});

Python/Django (Built-in):

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

# In template
<form method="post">
    {% csrf_token %}
    <input type="text" name="amount">
    <input type="text" name="to_account">
    <button type="submit">Transfer</button>
</form>

# In view
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def transfer_view(request):
    if request.method == 'POST':
        # CSRF token automatically validated
        amount = request.POST.get('amount')
        to_account = request.POST.get('to_account')
        transfer_money(amount, to_account)
    return render(request, 'transfer.html')

Ruby on Rails (Built-in):

# ApplicationController
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

# In view
<%= form_for @transfer, url: transfer_path do |f| %>
  <%= f.hidden_field :authenticity_token %>
  <%= f.text_field :amount %>
  <%= f.text_field :to_account %>
  <%= f.submit "Transfer" %>
<% end %>

# In controller
class TransfersController < ApplicationController
  def create
    # CSRF token automatically validated
    @transfer = Transfer.new(transfer_params)
    if @transfer.save
      redirect_to @transfer, notice: 'Transfer completed.'
    else
      render :new
    end
  end

  private
  def transfer_params
    params.require(:transfer).permit(:amount, :to_account)
  end
end

Token Security Best Practices

  1. Cryptographically Secure Generation:
  2. Use random_bytes() or equivalent
  3. Minimum 32 bytes of entropy
  4. Unique per session/form

  5. Token Scope:

  6. Per-session tokens for simplicity
  7. Per-form tokens for maximum security
  8. Per-request tokens for high-security operations

  9. Token Storage:

  10. Server-side session storage
  11. Avoid client-side storage (localStorage, cookies)
  12. Regenerate after successful authentication

  13. Token Validation:

  14. Strict comparison (=== in PHP, == in Python)
  15. Immediate failure on mismatch
  16. Log validation failures for monitoring

How It Works

The SameSite attribute controls when cookies are sent with cross-origin requests: - Strict: Cookies only sent for same-origin requests - Lax: Cookies sent for top-level navigation (GET requests) - None: Cookies sent for all cross-origin requests (requires Secure)

Implementation

Setting SameSite in PHP:

// Set session cookie with SameSite
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'domain' => 'example.com',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax'
]);

Setting SameSite in Express:

app.use(session({
    secret: 'your-secret',
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: true,
        httpOnly: true,
        sameSite: 'lax'
    }
}));

Setting SameSite in Django:

# settings.py
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True

Setting SameSite in Rails:

# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: "_app_session", same_site: :lax, secure: true

Browser Support and Fallbacks

// JavaScript fallback for older browsers
function setCookie(name, value, options = {}) {
    let cookieString = `${name}=${value}`;

    if (options.sameSite) {
        // Modern browsers
        cookieString += `; SameSite=${options.sameSite}`;
    }

    // Fallback for older browsers - use additional checks
    if (options.secure) cookieString += '; Secure';
    if (options.httpOnly) cookieString += '; HttpOnly';
    if (options.path) cookieString += `; Path=${options.path}`;
    if (options.maxAge) cookieString += `; Max-Age=${options.maxAge}`;

    document.cookie = cookieString;
}

3. Referer Header Validation

How It Works

Validate the Referer header to ensure requests come from trusted origins: - Check if the referer matches the expected domain - Reject requests with missing or suspicious referers - Combine with HTTPS to prevent header spoofing

Implementation

PHP Referer Validation:

function validateReferer($allowed_domains = []) {
    if (!isset($_SERVER['HTTP_REFERER'])) {
        return false;
    }

    $referer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);

    foreach ($allowed_domains as $domain) {
        if (stripos($referer, $domain) !== false) {
            return true;
        }
    }

    return false;
}

// Usage
if (!validateReferer(['example.com', 'www.example.com'])) {
    die('Invalid referer');
}

Express Referer Middleware:

const express = require('express');
const app = express();

function validateReferer(req, res, next) {
    const referer = req.get('Referer');
    const allowedDomains = ['https://example.com', 'https://www.example.com'];

    if (!referer) {
        return res.status(403).json({ error: 'Missing referer header' });
    }

    const refererOrigin = new URL(referer).origin;

    if (!allowedDomains.includes(refererOrigin)) {
        return res.status(403).json({ error: 'Invalid referer' });
    }

    next();
}

// Apply to sensitive routes
app.post('/transfer', validateReferer, (req, res) => {
    // Process transfer
});

Django Referer Validation:

from django.middleware.csrf import CsrfViewMiddleware
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

class RefererValidationMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.allowed_domains = ['example.com', 'www.example.com']

    def __call__(self, request):
        if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
            referer = request.META.get('HTTP_REFERER')
            if referer:
                try:
                    referer_domain = urlparse(referer).netloc
                    if not any(domain in referer_domain for domain in self.allowed_domains):
                        return HttpResponseForbidden('Invalid referer')
                except:
                    return HttpResponseForbidden('Invalid referer')
            else:
                return HttpResponseForbidden('Missing referer')

        return self.get_response(request)

Limitations and Considerations

  • HTTPS Required: Referer headers can be stripped over HTTP
  • Browser Behavior: Some browsers don't send referer for HTTPS → HTTP
  • Privacy Extensions: Some users disable referer sending
  • Mobile Apps: May not send referer headers
  • CDNs/Proxies: May modify or strip referer headers

4. Custom Header Requirements

How It Works

Require specific headers that can only be set by legitimate requests: - Custom headers like X-Requested-With: XMLHttpRequest - Headers that browsers don't automatically include - Headers that must be explicitly set by legitimate code

Implementation

Express Custom Header Check:

function requireCustomHeader(req, res, next) {
    const customHeader = req.get('X-Custom-Header');

    if (!customHeader || customHeader !== 'expected-value') {
        return res.status(403).json({ error: 'Missing or invalid custom header' });
    }

    next();
}

// Apply to API endpoints
app.post('/api/transfer', requireCustomHeader, (req, res) => {
    // Process transfer
});

Angular HTTP Client:

import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private headers = new HttpHeaders({
    'X-Custom-Header': 'expected-value',
    'Content-Type': 'application/json'
  });

  constructor(private http: HttpClient) {}

  transferMoney(amount: number, toAccount: string) {
    return this.http.post('/api/transfer', {
      amount,
      toAccount
    }, { headers: this.headers });
  }
}

React Fetch with Custom Headers:

const apiCall = async (endpoint, data) => {
    const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Custom-Header': 'expected-value',
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify(data)
    });

    return response.json();
};

Advanced Mitigation Techniques

How It Works

  • Set a random value in both a cookie and a request parameter
  • Server validates that both values match
  • Prevents CSRF while avoiding server-side state

Implementation

JavaScript Client:

// Generate random token
function generateToken() {
    return Math.random().toString(36).substring(2);
}

const csrfToken = generateToken();

// Set cookie
document.cookie = `csrf_token=${csrfToken}; Secure; SameSite=Lax`;

// Include in request
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        amount: 1000,
        toAccount: '12345',
        csrfToken: csrfToken
    })
});

Server Validation:

app.post('/api/transfer', (req, res) => {
    const cookieToken = req.cookies.csrf_token;
    const requestToken = req.body.csrfToken;

    if (!cookieToken || !requestToken || cookieToken !== requestToken) {
        return res.status(403).json({ error: 'CSRF validation failed' });
    }

    // Process request
});

2. Origin Header Validation

How It Works

Validate the Origin header (more reliable than Referer): - Check if the origin matches allowed domains - Reject requests from unauthorized origins - Combine with other validation methods

Implementation

Express Origin Validation:

function validateOrigin(req, res, next) {
    const origin = req.get('Origin');
    const allowedOrigins = [
        'https://example.com',
        'https://www.example.com',
        'https://app.example.com'
    ];

    if (origin && !allowedOrigins.includes(origin)) {
        return res.status(403).json({ error: 'Invalid origin' });
    }

    next();
}

3. Content-Type Validation

How It Works

Restrict requests to specific content types: - Only allow application/json for API endpoints - Reject form-encoded requests from different origins - Prevent simple form submissions

Implementation

Express Content-Type Check:

function requireJsonContentType(req, res, next) {
    const contentType = req.get('Content-Type');

    if (!contentType || !contentType.includes('application/json')) {
        return res.status(415).json({ error: 'Content-Type must be application/json' });
    }

    next();
}

// Apply to API routes
app.use('/api', requireJsonContentType);

4. Request Method Validation

How It Works

Ensure state-changing operations use appropriate HTTP methods: - Use POST/PUT/PATCH/DELETE for state changes - Avoid GET for any state-changing operations - Validate method consistency

Implementation

Method Validation Middleware:

function validateMethod(req, res, next) {
    const allowedMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];

    if (!allowedMethods.includes(req.method)) {
        return res.status(405).json({ error: 'Method not allowed' });
    }

    next();
}

Framework-Specific Mitigations

1. Spring Security CSRF Protection

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}

2. ASP.NET Core CSRF Protection

// Startup.cs
public void ConfigureServices(IServiceCollection services) {
    services.AddControllersWithViews(options => {
        options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
    });

    services.AddAntiforgery(options => {
        options.HeaderName = "X-CSRF-TOKEN";
    });
}

// In view
<form asp-action="Transfer" asp-controller="Account" method="post">
    @Html.AntiForgeryToken()
    <input asp-for="Amount" />
    <input asp-for="ToAccount" />
    <button type="submit">Transfer</button>
</form>

3. Laravel CSRF Protection

// In routes/web.php
Route::post('/transfer', function (Request $request) {
    // CSRF token automatically validated
    $amount = $request->input('amount');
    $toAccount = $request->input('to_account');

    // Process transfer
})->middleware('web'); // Includes CSRF protection

// In blade template
<form method="POST" action="/transfer">
    @csrf
    <input type="text" name="amount">
    <input type="text" name="to_account">
    <button type="submit">Transfer</button>
</form>

API-Specific Mitigations

1. REST API CSRF Protection

// API endpoint with multiple protections
app.post('/api/users/:id', [
    validateOrigin,
    validateCustomHeader,
    requireJsonContentType
], (req, res) => {
    const userId = req.params.id;
    const updates = req.body;

    // Additional authentication check
    if (req.user.id !== userId) {
        return res.status(403).json({ error: 'Unauthorized' });
    }

    updateUser(userId, updates);
    res.json({ success: true });
});

2. GraphQL CSRF Protection

const { graphqlHTTP } = require('express-graphql');
const { validateCsrfToken } = require('./csrf-middleware');

app.use('/graphql', graphqlHTTP({
    schema: MyGraphQLSchema,
    rootValue: root,
    validationRules: [validateCsrfToken]
}));

3. WebSocket CSRF Protection

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
    // Validate CSRF token from query parameter or header
    const csrfToken = req.url.split('csrf=')[1];

    if (!validateCsrfToken(csrfToken)) {
        ws.close(1008, 'CSRF validation failed');
        return;
    }

    // Handle WebSocket messages
    ws.on('message', (message) => {
        // Process message
    });
});

Mobile Application Mitigations

1. Android CSRF Protection

// OkHttp client with CSRF token
public class ApiClient {
    private static final String CSRF_TOKEN = "X-CSRF-Token";

    public static void makeRequest(String url, JSONObject data) throws IOException {
        OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new CsrfInterceptor())
            .build();

        RequestBody body = RequestBody.create(
            data.toString(),
            MediaType.parse("application/json")
        );

        Request request = new Request.Builder()
            .url(url)
            .post(body)
            .build();

        client.newCall(request).execute();
    }

    static class CsrfInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request original = chain.request();
            Request.Builder builder = original.newBuilder()
                .header(CSRF_TOKEN, getCsrfToken());

            return chain.proceed(builder.build());
        }
    }
}

2. iOS CSRF Protection

import Alamofire

class ApiManager {
    static let csrfToken = "X-CSRF-Token"

    func makeRequest(url: String, parameters: [String: Any]) {
        let headers: HTTPHeaders = [
            csrfToken: getCsrfToken(),
            "Content-Type": "application/json"
        ]

        AF.request(url,
                   method: .post,
                   parameters: parameters,
                   encoding: JSONEncoding.default,
                   headers: headers)
            .responseJSON { response in
                // Handle response
            }
    }
}

Monitoring and Detection

1. CSRF Attack Detection

// Middleware to detect suspicious patterns
function detectCsrfAttempts(req, res, next) {
    const suspiciousPatterns = [
        req.method === 'POST' && !req.get('Referer'),
        req.method === 'POST' && req.get('Origin') !== 'https://example.com',
        req.body && typeof req.body === 'object' && !req.body.csrfToken
    ];

    if (suspiciousPatterns.some(pattern => pattern)) {
        // Log suspicious activity
        console.log('Potential CSRF attempt:', {
            ip: req.ip,
            userAgent: req.get('User-Agent'),
            url: req.url,
            method: req.method
        });
    }

    next();
}

2. Rate Limiting

const rateLimit = require('express-rate-limit');

const csrfLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 5, // Limit each IP to 5 requests per windowMs
    message: 'Too many requests, please try again later',
    standardHeaders: true,
    legacyHeaders: false,
});

// Apply to sensitive endpoints
app.use('/api/transfer', csrfLimiter);

3. Logging and Alerting

function logCsrfEvent(req, eventType, details) {
    const logEntry = {
        timestamp: new Date().toISOString(),
        eventType,
        ip: req.ip,
        userAgent: req.get('User-Agent'),
        url: req.url,
        method: req.method,
        referer: req.get('Referer'),
        origin: req.get('Origin'),
        details
    };

    // Send to logging service
    logService.log('csrf_attempt', logEntry);

    // Send alert if critical
    if (eventType === 'csrf_success') {
        alertService.sendAlert('CSRF Attack Successful', logEntry);
    }
}

Testing CSRF Mitigations

1. Automated Testing

const puppeteer = require('puppeteer');

async function testCsrfProtection() {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // Navigate to legitimate site
    await page.goto('https://example.com/login');
    await page.type('#username', 'testuser');
    await page.type('#password', 'testpass');
    await page.click('#login');

    // Wait for login
    await page.waitForNavigation();

    // Try CSRF attack
    const csrfPage = await browser.newPage();
    await csrfPage.setContent(`
        <form action="https://example.com/transfer" method="POST">
            <input name="amount" value="1000">
            <input name="to" value="attacker">
        </form>
        <script>document.forms[0].submit();</script>
    `);

    // Check if attack succeeds
    const response = await csrfPage.waitForResponse('https://example.com/transfer');
    const status = response.status();

    if (status === 403) {
        console.log('CSRF protection working');
    } else {
        console.log('CSRF protection failed');
    }

    await browser.close();
}

2. Manual Testing Checklist

  • Remove CSRF tokens from requests
  • Test with different origins
  • Try GET requests for POST operations
  • Test with missing referer headers
  • Attempt JSON requests without proper headers
  • Test SameSite cookie bypasses
  • Verify token regeneration after login

Common Pitfalls and Mistakes

1. Incomplete Protection

// Bad: Only protecting some routes
app.post('/transfer', csrfProtection, handler);
app.post('/settings', handler); // Missing protection
app.put('/profile', handler);   // Missing protection

2. Weak Token Generation

// Bad: Predictable tokens
const token = Date.now().toString(); // Easily guessable

// Bad: Short tokens
const token = Math.random().toString(36).substring(2, 8); // Too short

3. Improper Token Validation

// Bad: Loose comparison
if (req.body.csrfToken != session.csrfToken) { // Type coercion

// Bad: No token rotation
// Token never changes after generation

4. CORS Misconfiguration

// Bad: Permissive CORS
app.use(cors({
    origin: true, // Allows any origin
    credentials: true // Allows cookies
}));

5. Missing HTTPS

// Bad: No HTTPS enforcement
app.listen(80); // HTTP only

// Bad: No secure cookie flags
res.cookie('session', sessionId); // Missing secure flag

Performance Considerations

1. Token Generation Overhead

// Optimized token generation
const crypto = require('crypto');

function generateSecureToken() {
    // Use crypto.randomBytes for better performance than random_bytes
    return crypto.randomBytes(32).toString('hex');
}

// Cache tokens for session
const tokenCache = new Map();

function getCachedToken(sessionId) {
    if (!tokenCache.has(sessionId)) {
        tokenCache.set(sessionId, generateSecureToken());
    }
    return tokenCache.get(sessionId);
}

2. Database Session Storage

// Efficient session storage
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);

const sessionStore = new MySQLStore({
    host: 'localhost',
    user: 'session_user',
    password: 'session_pass',
    database: 'sessions'
});

app.use(session({
    store: sessionStore,
    secret: 'your-secret',
    resave: false,
    saveUninitialized: false
}));

Compliance and Standards

1. OWASP Recommendations

  • Use CSRF tokens for all state-changing operations
  • Implement SameSite=Lax for session cookies
  • Validate Origin/Referer headers
  • Log and monitor CSRF attempts

2. Industry Standards

NIST SP 800-63B: - Multi-factor authentication reduces CSRF impact - Session management best practices - Token-based authentication guidance

ISO 27001: - Access control requirements - Information security incident management - Cryptographic controls

Conclusion

Effective CSRF mitigation requires a multi-layered approach combining several techniques:

  1. CSRF Tokens as the primary defense mechanism
  2. SameSite Cookies for additional browser-level protection
  3. Referer/Origin Validation for request origin verification
  4. Custom Headers for API endpoint protection
  5. Proper HTTPS Configuration to prevent header manipulation

Modern web frameworks provide built-in CSRF protection, but custom implementations require careful attention to security best practices. Regular testing, monitoring, and staying updated with emerging threats are essential for maintaining effective CSRF protection.

Remember: Defense-in-depth is key. No single mitigation technique is foolproof, but combining multiple layers creates robust protection against CSRF attacks.