Search code examples
ruby-on-railscsrf-protection

Why does the CSRF token in Rails not prevent multiple tabs from working properly?


After reading about how the CSRF protection works in Rails, I tried to trigger CSRF protection by doing this:

Note: We are using cookie based sessions.

  1. Visit login page. Check CSRF token in meta => abc123
  2. Open a 2nd browser tab, and visit the same login page. CSRF token in meta is different => def456
  3. Go back to 1st tab.
  4. Submit login credentials.

I expected this to fail, because the 2nd tab generated a new, different CSRF token. When the login form submits, shouldn't the token that gets submitted to the server be an old, stale one?

However, this does work:

  1. Visit login page. Check CSRF token in meta => abc123
  2. Open a 2nd browser tab, and visit the same login page. CSRF token in meta is different => def456
  3. Go back to 1st tab.
  4. Submit login credentials.
  5. Logout (clearing session)
  6. Go to 2nd tab, and submit login.

In this case, I get an InvalidAuthenticityToken exception as expected. Why?


Solution

  • Source: https://medium.com/rubyinside/a-deep-dive-into-csrf-protection-in-rails-19fa0a42c0ef

    Why the request in the second tab doesn't fail

    The CSRF token in the meta tag is actually a concatenation of two strings: a "one-time pad" generated per request, and the "real" CSRF secret XORed with the one-time pad. See in the diagram below how the one-time pad is prepended to the XORed string in the masked token, which gets stored in the meta tag:

    Diagram of construction of CSRF token

    Rails stores the CSRF secret in a session cookie without XORing. Javascript should be used in the browser to read the masked token from the meta tag and pass it in the X-CSRF-TOKEN header.

    When Rails validates a request, it:

    1. Splits the value passed in the X-CSRF-TOKEN header to retrieve the one-time pad and XORed string.
    2. XORs them together to retrieve the real secret.
    3. Compares this with the secret in the cookie.

    This is why you are seeing changing tokens in the meta tag -- the one-time pads are different. If you validated the tokens, you would find the same secret in both tokens.

    Note: This one-time pad business might seem unnecessary. Anyone can retrieve the real secret if they have the masked token. Surprisingly, the purpose of the XORing is to change the CSRF token on every request so an attacker can't use timing attacks to discern the secret. See this paper on the BREACH SSL attack.

    Why the request fails on logout

    As noted in @max's comment, logging out deletes the session cookie. The next request generates a new CSRF secret which no longer matches the older masked tokens.