Cross-Site Request Forgery (CSRF) Attacks

Understanding CSRF vulnerabilities and implementing effective protection mechanisms

Last updated:

Cross-Site Request Forgery (CSRF) Attacks

Cross-Site Request Forgery (CSRF) is a type of attack that tricks a user’s browser into executing unwanted actions on a website where the user is authenticated. Unlike XSS, which exploits the trust a user has in a particular site, CSRF exploits the trust that a site has in a user’s browser.

How CSRF Attacks Work

CSRF attacks work by taking advantage of websites that rely solely on session cookies to authenticate users. When a user is authenticated to a site, their browser automatically sends the session cookie with every request to that site. A CSRF attack tricks the victim’s browser into sending a malicious request to the vulnerable website.

Attack Scenario

  1. User logs into bank.com and receives a session cookie
  2. Without logging out, the user visits malicious.com
  3. malicious.com contains code that submits a form to bank.com/transfer
  4. The browser automatically includes the user’s bank.com cookies with the request
  5. bank.com processes the request as legitimate since it includes valid session cookies

Example CSRF Attack

HTML Form-based Attack

<!-- On malicious.com -->
<html>
  <body>
    <form id="csrf-form" action="https://bank.com/transfer" method="POST">
      <input type="hidden" name="recipient" value="attacker" />
      <input type="hidden" name="amount" value="1000" />
    </form>
    <script>
      document.getElementById("csrf-form").submit();
    </script>
  </body>
</html>

Image-based Attack

<img src="https://bank.com/transfer?recipient=attacker&amount=1000" width="0" height="0" />

XMLHttpRequest Attack

var xhr = new XMLHttpRequest();
xhr.open("POST", "https://bank.com/transfer", true);
xhr.withCredentials = true; // This is needed to include cookies
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("recipient=attacker&amount=1000");

CSRF Prevention Techniques

1. CSRF Tokens

The most effective defense is to include a unique, unpredictable token with each request that requires protection:

<form action="/transfer" method="post">
  <input type="hidden" name="csrf_token" value="random_token_tied_to_user_session" />
  <input type="text" name="recipient" />
  <input type="number" name="amount" />
  <button type="submit">Transfer</button>
</form>

Server-side validation:

# Python/Flask example
@app.route('/transfer', methods=['POST'])
def transfer():
    if request.form['csrf_token'] != session['csrf_token']:
        abort(403)  # Forbidden
    # Process the transfer

2. Same-Site Cookies

Set the SameSite attribute on cookies to restrict when they are sent:

Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly
  • Strict: Cookies are only sent in a first-party context
  • Lax: Cookies are sent when the user navigates to the site from an external link
  • None: Cookies are sent in all contexts (requires Secure attribute)

3. Custom Request Headers

For AJAX requests, leverage the Same-Origin Policy by adding a custom header that can only be set by JavaScript from the same origin:

// JavaScript
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  },
  body: JSON.stringify({ recipient: 'friend', amount: 100 })
});

Server-side validation:

// Node.js example
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
  return res.status(403).send('Forbidden');
}

Set a cookie with a random value and include the same value as a hidden field or request parameter:

// Set the cookie
document.cookie = "csrfCookie=random_value; SameSite=Strict; Secure";

// Include the value in the request
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': 'random_value'
  },
  body: JSON.stringify({ recipient: 'friend', amount: 100 })
});

5. Require User Interaction

For sensitive operations, require additional user interaction:

  • Re-authentication for sensitive actions
  • CAPTCHA challenges
  • Confirmation steps

CSRF in Modern Web Applications

Modern web frameworks typically include built-in CSRF protection:

React with Axios

// Set up Axios to include the CSRF token
import axios from 'axios';

// Get the CSRF token from a meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

// Configure Axios to include the token in all requests
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken;

Django

# Django automatically includes CSRF protection
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def transfer_money(request):
    if request.method == 'POST':
        # Process the transfer
        pass

Testing for CSRF Vulnerabilities

Manual Testing

  1. Authenticate to the target application
  2. Identify state-changing operations (transfers, profile updates, etc.)
  3. Create a simple HTML page that submits a form to the target endpoint
  4. Test if the operation succeeds without additional verification

Automated Testing

Tools for CSRF detection:

  1. OWASP ZAP
  2. Burp Suite
  3. CSRF Tester

Real-World CSRF Examples

  1. Gmail (2007): A CSRF vulnerability allowed attackers to change users’ email settings
  2. ING Direct (2008): A CSRF vulnerability allowed attackers to transfer money from victims’ accounts
  3. YouTube (2008): A CSRF vulnerability allowed attackers to perform actions on behalf of users

Conclusion

CSRF attacks remain a significant threat to web applications that don’t implement proper protection mechanisms. By understanding how these attacks work and implementing appropriate countermeasures, developers can effectively protect their users from CSRF vulnerabilities.

References

  1. OWASP CSRF Prevention Cheat Sheet
  2. Mozilla Web Security
  3. SameSite Cookies Explained

Tags

csrf web-security authentication session-management