"The secret to strong security: less reliance on secrets." – Whitfield Diffie

Web developers must protect their apps against Cross-Site Request Forgery (CSRF) attacks. If they don't, a hacker controlling some other web site could trick the app into taking action on behalf of an innocent user. Typically, web servers and browsers exchange secret CSRF tokens as their primary defense against these exploits.

CSRF tokens are traditionally stored either by the web server or in an encrypted cookie in the web browser. This article looks at an alternative approach to CSRF protection: on-demand, cryptographically signed tokens that require no storage.

Quick History of CSRF Protection

In all cases, the app server generates a token, stores it somewhere, and sends it to the browser. The browser includes this token in mutable requests, and the app checks to make sure it is valid.

The first generation of CSRF protection just stored tokens on a local disk. Eventually, apps had to scale beyond a single server and used a shared database to store the CSRF token. This approach is effective, but it adds unnecessary load on the database server.

Today's web frameworks use encrypted cookies to store the token in the browser. When making a request, the browser includes both the raw CSRF token and the encrypted cookie (containing the token). The server can decrypt the cookie and verify that the two tokens match. This cookie-to-header token scheme is secure because the browser has no way to decrypt the contents of the cookie.

This partial HTTP response shows the encrypted cookie _gorilla_csrf containing the CSRF token, as well as the X-Csrf-Token header with the raw token:

< HTTP/1.1 200 OK
< Set-Cookie: _gorilla_csrf=MTU4Mzg3Nzg4M3xJa2hhVUVSUU9WaFphSEJhV1RCSlNIQjJTbWhtU1cxWVMyeDZNM1JyWW1kSFFXVk5SVzlLZHk4dlJITTlJZ289fJoHPWZobIWPupouH6OGXF311g4FZO6wk5VVdWqFuKah; Expires=Wed, 11 Mar 2020 10:04:43 GMT; Max-Age=43200; HttpOnly; Secure; SameSite
< Vary: Cookie
< X-Csrf-Token: FYPX7DS5pqZ2AnuHC6PiAnDrcjEz/WQ8PXdivxLqjf4IEBTT4WEgMC7S+m63O70gFSHlDN5s3Do8lGYfjtVxxQ==

The double-submit-cookie scheme is similar, except the browser sends the raw CSRF token in a separate cookie. It is used by platforms like Django and Rails.

The conventional cookie-to-header and double-submit-cookie approaches are analogous to having the server store the token in the database. Instead of looking up the token in a database, the server "looks up" the token by decrypting the cookie sent from the browser. Then, in both cases, the server compares that token to the one sent with the browser's request.

Stateless CSRF Protection

Moving the token storage from server to client was a great innovation. The server no longer has to worry about it, and encryption keeps the token secure while the browser stores it. We have moved the "state" from your server to the user's browser.

Can we do better than this? Consider the benefits of a completely "stateless" solution:

  • Works on browsers that have cookies disabled
  • Supports WebSockets, which don't work with cookies
  • Reduces bandwidth, since the encrypted CSRF cookie doesn't get shipped with each request
  • Doesn't require a database or server-side storage

HMAC Based CSRF Tokens

All of the anti-CSRF techniques described above involve the app server generating a token, which is encrypted so the browser can't read it. An often-overlooked alternative is to have the server create a cryptographically signed token.

The token doesn't have to be hidden from the client, because it can only be created (signed) with a secret key on the server. Only the server can cryptographically validate the token so that an attacker cannot forge or tamper with it. Thanks to this validation, the server doesn't need to store the token anywhere.

When the browser makes a request, it includes the CSRF token just like usual. The server doesn't have to look anything up; it can use its secret key to confirm the token is valid. HMAC is used to create the signed token, so this architecture is officially dubbed the HMAC Based Token Pattern.

Sequence diagrams for Cookie-to-header and HMAC CSRF token schemes

This technique is lightweight and flexible by nature. Modern JavaScript applications can store the token wherever they please: application state (Redux/Vuex/etc.), browser localstorage, or even a cookie.

When making requests, the browser includes the CSRF token in a header field. This matches the other schemes described above; the only difference is that no encrypted CSRF cookie is required.

If you combine stateless CSRF with token-based sessions, your app might not even need cookies at all. Get your analytics privacy right, and you could say goodbye to Cookie Law popups.

In all, a win for performance, compatibility, and convenience.

Better CSRF Protection for Developers

🚨 If your web framework already provides CSRF protection, by all means use that! Security is generally a bad place to spend your innovation tokens.

The CSRF token gets created by applying an HMAC hash to the timestamp and session ID, which is then Base64 encoded.

func generateCSRFHash(ts, sessionid, key string) (string, error) {
    if len(key) < MIN_KEY_LENGTH {
        return "", fmt.Errorf("Key too short")
    }
    body := []byte(ts)
    body = append(body, []byte(sessionid)...)
    mac := hmac.New(sha256.New, []byte(key))
    mac.Write(body)
    return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
}

The server sends the CSRF token to the client, usually in a header. Later, when the client sends the token back, the server can validate it without cookies or server-side storage.

const TSLen = 19

func ValidateCSRFToken(uid, key, token string, valid time.Duration) error {
    if len(token) < TSLen+1 {
        return fmt.Errorf("CSRF token too short")
    }
    ts := token[:TSLen]
    hash := token[TSLen:]

    expectedHash, err := generateCSRFHash(ts, uid, key)
    if err != nil {
        return fmt.Errorf("CSRF hash error")
    }
    if !hmac.Equal([]byte(hash), []byte(expectedHash)) {
        return fmt.Errorf("CSRF token invalid")
    }
    issuedTs, err := strconv.ParseInt(ts, 10, 64)
    if err != nil {
        return fmt.Errorf("CSRF timestamp invalid")
    }
    issued := time.Unix(0, issuedTs)
    if issued.Add(valid).Before(time.Now()) {
        return fmt.Errorf("CSRF token expired")
    }
    return nil
}

There are existing CSRF HMAC implementations for NodeJS and Java, but I haven't tried them. It feels like there is an opportunity to add CSRF HMAC support to many other web frameworks.

No matter what approach you use for CSRF protection, it is worthless without proper configuration. Use HTTPS and Origin checks, and check referer headers. Ensure that your CORS policy is strict. Yeah, a * wildcard doesn't count.

If you're using cookies, consider SameSite, Secure, and HttpOnly options, as well as a Vary: Cookie header to avoid caching. And never roll your own encryption.

All anti-CSRF schemes have one other fatal weakness. It's game over if an attacker can run malicious JavaScript in your client's browser.

Every web and API developer should be familiar with Cross-Site Scripting (XSS), one of the most common vulnerabilities on the internet.

All anti-CSRF schemes have one other fatal weakness, as every web and API developer should know: Cross-Site Scripting (CSS) attacks, one of the most common vulnerabilities on the internet. It’s game over if an attacker can run malicious JavaScript in your client’s browser. Review your CSS controls and make sure they meet the current recommendations.

Aloha

Find this approach compelling? Spy a glaring security vulnerability? I'm focused where software, infrastructure, and data meets security, operations, and performance. Follow or DM me on twitter at @nedmcclain.