Recently at Auto Trader, we’ve been busy overhauling the stack behind the ‘My Auto Trader’ area. In our previous stack, we’d implemented our own solution to protect our customers from CSRF (Cross-Site Request Forgery).

As we migrated the back-end to Spring Boot (a Java-based, open-source framework used to create production-grade web applications), we were able to use Spring Security to prevent this lesser-known vulnerability. Here’s how we did this, integrating it into our new GraphQL powered React front-end.

What is CSRF?

Cross-site request forgery (or CSRF) is a lesser-known security vulnerability that allows an attacker to coax a user into unknowingly performing actions against a web application for which they are currently authenticated.

This is carried out by sending a request to the web application on behalf of the user. Web applications that are vulnerable to this kind of attack cannot distinguish between a genuine request sent by a user and a malicious request sent by the user but unbeknownst to them.

This vulnerability requires a little social engineering to carry out the attack but it could be something as simple as an email or a text message requesting that the user clicks a link. Upon clicking the link, and because the user is currently logged in, the application will then proceed to make a change to the user’s account.

What can an attacker do with CSRF?

A successful CSRF attack could be as detrimental as transferring funds from the user’s account to that of the attacker (in the case of a banking website), or it could change the password to one that is chosen by the attacker, giving them full control of the user’s account.

In the case of Auto Trader, examples might be the modification of a user’s advert to contain the attacker’s telephone number who might then proceed to take vehicle deposits.

Alternatively, in the ‘My Account’ area, although significantly less severe, an attacker could mischievously delete saved payment methods, which will affect the user experience and potentially drive calls into our call centre.

Other examples include:

How does CSRF work?

To explain how CSRF works, let’s look at a timeline of events. In this example, we’ll see how this vulnerability could lead to an account takeover (when an attacker takes complete control of a victim’s account).

Diagram showing the stages of an account takeover CSRF attack

It’s worth noting that in this scenario, the attack is possible because the request sent to the application does not contain any unguessable or indeterminable parameters that are required to successfully perform the password reset.

CSRF attacks also work regardless of the request method used (GET, PUT, POST etc). However, care must be taken to ensure that Safe HTTP methods such as GET, HEAD, OPTIONS, and TRACE methods are read-only and don’t modify state on the server.

CSRF works because there isn’t anything built into HTTP that you can use to distinguish between an HTTP request triggered by a user clicking a link (or submitting a form) on a legitimate website, or an attacker’s website.

In other words, without any specific CSRF prevention methods in place, it would be impossible to reject requests from an attacker’s website and accept those from autotrader.co.uk.

Now that we know how CSRF works, let’s look at how we might mitigate this vulnerability.

Can’t you use the ‘Referer’ header?

The Referer header in an HTTP request contains the URL of the page where the user clicked the link. So at first glance, it appears that it could be used to provide some form of protection against CSRF.

The application could use this header to inspect where the traffic is coming from and block requests that have other domains in the Referer header.

However, there are several problems with this:

Therefore, the Referer header is not something we could rely upon. We needed to provide something additional within our requests that an attacker cannot provide on their malicious website.

There is one important bit of context to cover before we get into the techniques that we settled on to protect against CSRF. Our Single Page App (SPA) architecture here at Auto Trader meant that we had to consider making this change in a couple of places.

Single Page App architecture

Auto Trader single page app architecture As Wes described at an AT Tech Talks event, much of our front end is delivered by SPAs built using create-react-app.

Between the SPAs’ React front end and our back-end services and databases, we have our internal GraphQL server. This aggregates the responses of RESTful web services into one single response that can be easily consumed by our SPAs.

One of the RESTful web services that our GraphQL server calls is our internal Private Advert Service. This is a Spring Boot service that allows users to retrieve and delete payment methods (amongst other things).

One of the main design decisions we took during this project was to secure the Private Advert Service rather than fully incorporate CSRF protection into GraphQL for all of our SPAs to take advantage of. The main reason for this was because other clients (such as Carzone) planned to hit our service without going via GraphQL.

Within our Private Advert Service, we decided to use Spring Security and the Double Submit Cookie Pattern as a mechanism to prevent CSRF attacks on our payment methods endpoints.

However, we had to implement it in such a way that worked well with GraphQL sitting in the middle. Here’s how it works in practice. There are two parts to the journey we wanted to secure:

  1. First, the user retrieves their payment methods with a read-only request
  2. Then, they may choose to delete a payment method from the details presented to them

Retrieving Payment Methods

Logged-in users manage their ‘Payment Methods’ in the ‘My Account’ section (see screenshot).

Here a user can see the payment cards that they have saved when paying for an advert.

Screenshot of the Payment Methods page in ‘My Account

The SPA fetches the card details by making a POST request with a GraphQL query to our GraphQL server.

This query and the fields defined in it are defined in our GraphQL schemas. These define what data a client (such as an SPA) can read and write to the GraphQL server.

We also created a resolver that is responsible for populating the data defined in our schema.

Spring Boot CSRF protection

The resolver in this case communicates with a GET endpoint in our Private Advert Service, to retrieve a list of the user’s payment methods.

Not only do we receive a list of payment methods from our service, we also receive a CSRF token in the form of a cookie since CSRF protection is enabled by default in Spring Boot.

We configured this default protection by using a .crsfTokenRepository() in a Spring Security configuration class that extends WebSecurityConfigurerAdapter:

http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

CsrfTokenRepository is a CSRF protection strategy and CookieCsrfTokenRepository is the actual implementation of the Double Submit Cookie Pattern provided by Spring Security.

We create the CookieCsrfTokenRepository .withHttpOnlyFalse() so that cookies are readable from a JavaScript application.

When the GET /payment-methods endpoint in the Private Advert Service is invoked, Spring Security intercepts the request and calls through to the CookieCsrfTokenRepository which generates a unique token that is sent in a cookie to be parsed by GraphQL.

Obtaining the CSRF token in the GraphQL layer was simply a matter of parsing the token from the set-cookie header returned by the GET /payment-methods endpoint:

private paymentMethods = (response): PaymentMethodsCollection => {
    return {
        paymentMethodsList: response.result.paymentMethods,
        csrfToken: getCSFRTokenFromResponse(response),
    }
}

export const defaultCSRFCookieName = 'XSRF-TOKEN'
export const defaultCSRFHeader = 'X-XSRF-TOKEN'

export function getCSFRTokenFromResponse<T = unknown>(
    response: IRestResponse<T>,
    csrfCookieName = defaultCSRFCookieName
): string {
    return cookie.parse(response.headers['set-cookie'].toString())[csrfCookieName]
}

Once we obtained the CSRF token from Spring, we were then in a position to send the token to the client (our SPA) in a cookie.

This CSRF cookie is configured with the following cookie options:

Domain: ‘autotrader.co.uk’ This specifies which hosts are allowed to receive this cookie.

HttpOnly: false Setting HttpOnly to true prevents the cookie from being accessible to the JavaScript API. Whilst this helps to mitigate cross-site scripting (XSS) attacks, we needed our React app to be able to ‘read’ this CSRF cookie so that it can send it back to our server to be validated.

Secure: true Setting Secure to true ensures that our CSRF token is only sent to and from our server over the HTTPS protocol and never over unsecured HTTP. This makes it more difficult for the cookie to be accessed by a Man-in-the-Middle attacker.

SameSite: ‘strict’ The SameSite attribute specifies whether to allow cookies to be sent in cross-origin requests. Setting it to strict ensures that the cookie will only be sent to and from the server if the website for the cookie matches that shown in the browser’s address bar. This attribute is important as it provides some protection against CSRF attacks.

Deleting Payment Methods

This is the second and final part of the journey. Deleting ‘Payment Methods’ from a user perspective involves removing their payment card that they may have saved after purchasing an advert.

Since this changes the state of a user’s account, we needed to protect this endpoint against CSRF attacks.

When the user clicks ‘Remove Card’ in the UI, the React app reads the CSRF token from the cookie and then supplies the token value to a X-XSRF-TOKEN header and makes a POST request to the GraphQL server.

GraphQL then has the responsibility of proxying on the header and the cookie through to the Private Advert Service to be verified by the CookieCsrfTokenRepository before the DELETE /payment-methods endpoint is called.

This functionality can also be represented in a sequence diagram:

CSRF Flow

Verifying the CSRF token

When Spring Boot receives a request for an endpoint that is protected against CSRF, the CsrfFilter will intercept the request, read the token both from the cookie and the header, and check that they are equal.

Only our website can read the CSRF token from the cookie (due to the Same-origin policy). Although other websites can send a header value, they can’t read or set the cookie. This means that only our website can submit a header value that matches the cookie value. By checking if the values are equal, the app can decide whether the request will be allowed to continue onto the DELETE /payment-methods endpoint. This is the bit referred to by the name Double Submit Cookie Pattern because the same value is submitted in two different ways.

The values not being equal, or missing (either in the header, cookie or both), means that the request must not have originated from our web page. In this case, the request endpoint will return a 403 (forbidden) back to the client (GraphQL).

Limitations of this approach

The downsides to the approach that we have taken here are that there are known exploits that allow headers to be set by another domain. This approach is also vulnerable to man-in-the-middle attacks.

It also requires that a site has full control over its sub-domains and that they only accept connections over secure HTTP. The advantage of this approach is that it is stateless and was relatively easy to implement and scale.

A more secure approach would be to store CSRF tokens on the server-side and closely associate them with userIds and perhaps sessions. Whilst this would prevent token reuse, this would require additional engineering and complexity.

Summary

In summary, we felt that our stateless approach to addressing this vulnerability was ‘good enough’ given the severity of the attack we are trying to prevent. Our chosen approach, we feel, has the optimum mix of security, complexity and scalability and goes some way towards protecting our customers online.

Enjoyed that? Read some other posts.