A Crash Course in CSRF and XSS

Don’t let the bad guys (w)in!

James Collerton
8 min readDec 30, 2023
Hacking into the mainframe with CSS

Audience

This article is aimed at developers with at least a small amount of front-end experience, and a solid understanding of HTTP.

Within the article we will examine cross-site request forgery (CSRF) and cross-site scripting (XSS) attacks, including the relationship between the two. We will also cover some methods of protection.

Argument

What is CSRF?

Cross-Site Request Forgery (CSRF) is where an authenticated user is tricked into executing state-changing actions by an attacker. Examples include transferring money, changing account details, or messaging friends.

Imagine we have a banking website, where the site uses a cookie to determine if a user is authenticated or not. As browsers automatically include cookies for the server they were set by (or a specified domain), then the one demonstrating a user’s authentication will be sent along with any request for the banking site.

Now, exchanging money is done using a GET request. The amount and recipient is determined using query parameters to and amount.

http://your-bank.co.uk/send?to=james&amount=10

If you’re authenticated and I can convince you to open the URL above in your browser, you’ll send me ten pounds!

I could do this using a normal link:

<a href="NAUGHTY LINK HERE!">Another great Medium article by James</a>

Or a fake image, where the browser will attempt to load the URL in order to display the picture:

<img src="NAUGHTY LINK HERE!" width="0" height="0" border="0">

The above represent some potential attack vectors, but there are a range of social engineering tricks harmful parties can employ to get you to open the malicious URL.

The two examples above rely on the request being a GET. However, an intelligent and nefarious engineer can similarly emulate a POST, PUT or DELETE.

Protecting against CSRF using tokens

The most common protection against CSRF is the synchronizer token pattern. This means any state-changing request must contain a token not automatically included in requests by the client (e.g. not in the cookie).

Additionally, GET, HEAD, OPTIONS and TRACE should be read only (not state-changing).

Let’s update our example to use a POST request and the below form:

<form method="post"
action="/send">
<input type="text"
name="to"/>
<input type="text"
name="amount"/>
<input type="submit"
value="Send!"/>
</form>

Which will generate:

POST /send
Host: your-bank.co.uk
Cookie: SESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=10.00&to=james

An attacker could produce this form, put it on a site, and automatically send it on our behalf (including our cookie for the banking website). What we need is a CSRF token.

<form method="post"
action="/send">
<input type="hidden"
name="_csrf"
value="<randomly-generated-value>"/>
...

Now when the server receives a form submission it expects a token value it has generated, and can match to your session.

As malicious sites cannot produce a valid token, they can no longer commit the attack!

A visual representation of a CSRF mitigation using a token.

There are also alternatives for stateless applications that use cookies.

Another useful remediation is setting the SameSite property on your authentication cookies. This prevents them being attached unless the page sending them has been navigated to from another on the correct domain.

However, make sure this is something you actually want. It can cause issues (says the voice of experience).

Same-origin policy (SOP) and cross-origin resource sharing (CORS)

In the fledgling days of the internet CSRF issues were rampant. To combat this, browsers clubbed together and implemented the same-origin policy.

By default a client can only request resources with the same origin as the client’s URL. The protocol, port, and hostname must match all server requests.

For example, pretend a user is on https://nefarious-blog.co.uk, and I’m trying to get them to load the previous hidden image:

<img src="http://your-bank.co.uk/send?to=james&amount=10" width="0" height="0" border="0">

The browser will stop this happening as the protocol (http vs https) and hostname (nefarious-blog.co.uk vs your-bank.co.uk) are different.

Naturally there are cases where we want to allow this. For example, imgur needs to embed images.

To achieve this the owner of the requested resource (in our case your-bank.co.uk) requires a cross-origin resource sharing (CORS) policy.

Let’s imagine we have a valid site, your-bank-blog.co.uk, which we wanted to let load our central your-bank.co.uk. This is achieved by adding a line similar to the below to the your-bank.co.uk configuration file.

Access-Control-Allow-Origin: https://your-bank-blog.co.uk

We then have the below choreography:

  1. An administrator at your-bank.co.uk amends the server configuration file as above.
  2. A user loads your-bank-blog.co.uk in their browser.
  3. The browser requests resources from your-bank.co.uk. In doing so the browser adds the Origin header to the request with the value https://your-bank-blog.co.uk.
  4. The your-bank.co.uk server checks the Origin header value against the access list. It’s there!
  5. The correct resources are returned from your-bank.co.uk along with the header Access-Control-Allow-Origin : your-bank.co.uk, telling the browser everything is above board.

There a few clarifications worth making. Initially, this is mainly protection in the browser. It prevents users unwittingly loading scripts they don’t want while surfing the internet. If you’re hacking away at home you can set the Origin header to whatever you like.

Secondly, there is the notion of preflight requests. These are for complex requests (full definition here, but generally those using unusual methods, headers or data-types).

Instead of sending the Origin header with the request for data, they do an alternative roundtrip making sure the server is happy first, then send the data.

Finally, we can have credentialed requests. This is where requests are aware of HTTP cookies and HTTP Authentication information.

In our example we would attach our cookie for your-bank.co.uk to our request, and the server would return an additional Access-Control-Allow-Credentials: true header. Without the returned header the browser would ignore the response.

What does XSS have to do with it?

OK, so far so good! CSRF covers the case where a malicious party writes code and tries to execute it on their website. Cross-site-scripting (XSS) is the case where a malicious party writes code and tries to execute it on your website.

It is particularly dangerous as from the browser’s perspective it can’t tell which code is being validly sent from the web application and which is not. The latter will still have access to cookies, tokens and other valuable information.

Let’s take a really simple (and probably impossible) example. The site your-bank.co.uk has a search bar. If you load the URL your-bank.co.uk?search=james it will load the search bar with the phrase james pre-populated. The HTML resembles the below.

<input type="search" value="james"/>

An attacker tricks you into loading the URL (ignoring encoding)

your-bank.co.uk?search=james"/><script>stealMoney()</script>

This renders

<input type="search" value="james"/><script>stealMoney()</script>/>

Now the attacker is able to execute their malicious stealMoney script in your browser!

XSS takes one of three forms.

  1. Reflected XSS: When an application employs data from an HTTP request in an unsafe way. The above is an example of reflected XSS.
  2. Stored XSS: When we use stored data in an unsafe way. For example, if someone comments a malicious script on our blog, then every time a user views the comment it does something undesirable.
  3. DOM-based XSS: When client-side JavaScript uses data in an unsafe way. For example, if we get the value of a text input, then write that back into the HTML for the web page.

There are extra layers to XSS, but this is the core of it.

There are two main protections: input validation and output encoding.

Input validation for protecting against XSS

This is exactly what it sounds like. This includes things like:

  • If you’re expecting a number, make sure it doesn’t contain other characters.
  • If you’re expecting a URL, make sure it begins with a valid protocol (e.g. http or https instead of javascript).

Then any other sensible rules for your application!

A good rule is to use allowlists over blocklists. For example, if you want to only allow integers, permit only the characters 0-9. Don’t try and block a-z.

There are occasions when you would like to allow someone to input HTML. The general guidelines (which from lived experience I totally agree with), are to avoid this.

However, if you do go down this route one option is to have an allowlist of HTML tags (<b>, <i> etc.) and remove any potentially harmful ones (<script>, <a>). This should be done in the browser (using libraries like DOMPurify), and checked on the server.

An alternative is to have a separate markup language in the browser, which is then converted to HTML in the backend.

However, due to browser discrepancies, and the natural occurrence of vulnerabilities, this isn’t perfect.

Output encoding for protecting against XSS

Properly validating data on the way in does not excuse us from escaping it on the way out. We should avoid treating user input as code, and instead treat it as text.

How encoding works is framework and context-dependent. A useful OWASP list is here. To demonstrate the overarching idea we use an HTML example.

Imagine we have the below in our application. We want to allow the user to enter text in a separate input component, then render it in the below div:

<div> $userVariable </div>

If we allow them to enter <script>alert('hello!')</script> in the input field, then on rendering it we will receive the potentially harmful:

<div> <script>alert('hello!')</script> </div>

Disaster! Instead we need to encode the input on display. This translates the script component to:

&lt;script&gt;alert(&#39;hello!&#39;)&lt;/script&gt;

Which renders to what the user initially entered, but won’t execute as a script!

Content Security Policies (CSPs)

The final thing we will look into is how CSPs can protect against XSS. CSP is a way of defining which domains a browser should source executable scripts from. They can also be used to specify which protocols are permitted. For example, we may limit ourselves to https.

This means if an attacker embeds a nefarious script loaded from their own site, our application won’t load it as it comes from outside our permitted domains.

It allows us to safely load scripts from outside our own domain (circumventing the same-origin policy we discussed previously).

CSP is enabled through the Content-Security-Policy HTTP header (or <meta> tag) returned from the server. This tells the browser which types of data it can load from where, and how.

So how do we write a policy?

A policy is made up of policy directives which are documented here. A basic example so you can get the gist is below.

Content-Security-Policy: default-src ‘self’ your-bank.co.uk *.your-bank.co.uk

The default-src directive is the default for resource types which don’t have a specified policy. This is equivalent to the same-origin policy.

The other two parts allow all resources from our your-bank.co.uk domain and subdomains.

There are directives for limiting font, image, audio, video loading, along with many other resource types. Additionally, you can do things like add a script-src directive to stop inline scripts running, and a style-src directive to stop inline styles being applied.

Conclusion

In conclusion we have rocketed through CSRF, XSS and some of the ways you may protect against them.

--

--

James Collerton

Senior Software Engineer at Spotify, Ex-Principal Engineer at the BBC