csrf

CSRF Tokens vs Session Cookies


AFAIK, we define CSRF as security vulnerabilities that make the user perform a user-specific action without the user's consent by using various scripts by a malicious attacker. For example, our malicious attacker convinced the user to enter the website where the following code is running to change the user's email address.

<form action="https://examplefornonsafewebsite.com/email/change" method="POST">
    <input type="hidden" name="email" value="malicioususer@example.com">
</form>

<script>
    document.forms[0].submit();
</script>

In such a case, if the website does not have CSRF protection, the user's email address will be changed without the user's consent as a result of an action taken by the user. My question is, "What separates the CSRF token from a session cookie?" How can the session cookie be accessed but not the CSRF token while the malicious attacker is making the user change? After all, since this is done from the user's browser, shouldn't the CSRF token be accessible as well?


Solution

  • A session cookie should be HttpOnly and is then not accessible via Javascript, but it is automatically included in every request to the examplefornonsafewebsite.com domain from which it was issued.1

    To make the examplefornonsafewebsite.com web page safe, it must obtain an CSRF token prior to the making the POST request. This token is valid only in combination with ("is bound to") the session cookie and is obtained

    fetch("https://examplefornonsafewebsite.com/email/change", {
      method: "POST",
      headers: {"Content-Type": "x-www-form-urlencoded"},
      body: "email=malicioususer@example.com&X-CSRF-Token=..."
    })
    

    The examplefornonsafewebsite.com web page can use either option to include the CSRF token when it makes an email change request.

    But neither option is possible for the attacker: In the first option, the browser has already navigated to the examplefornonsafewebsite.com web page and the attacker's page is no longer involved at all.

    In the second option, the attacker's page can make the fetch request, but will not be able to read the response, because this is a cross-origin (CORS) request: It is made by Javascript from the attacker's web page but goes to the different origin https://examplefornonsafewebsite.com/. Then, since the response contains no Access-Control-Allow-Origin header, the fetch request is rejected with a "TypeError: Failed to fetch" and the attacker is not able to read the CSRF token.

    Importantly, if the attacker tried to obtain a CSRF token prior to the attack (by making a request to https://examplefornonsafewebsite.com/email/gettoken with their own browser), they might get one, but it would not be bound to the victim's session cookie, so it would be worthless.

    To summarize: The session cookie2 cannot be accessed via Javascript, neither by an attacker nor by the legitimate web page, but will be sent by the browser regardless. The CSRF token can be obtained only by the legitimate web page, by virtue of the CORS protocol (same-origin policy). Only by combining both do you ensure that

    The CSRF protection mechanism therefore relies on CORS.

    1 This can be restricted by setting the cookie to SameSite=Strict, which prevents cross-site (although not cross-origin) request forgery attacks without the need for a CSRF protection token. But it also prevents users from navigating cross-site just to see their data. See also here.

    2 This mechanism works even if the cookie in question is not a session cookie, see Should CSRF protection token be given before authenticating?