A Crash Course in CSRF and XSS
Don’t let the bad guys (w)in!
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!
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:
- An administrator at
your-bank.co.uk
amends the server configuration file as above. - A user loads
your-bank-blog.co.uk
in their browser. - The browser requests resources from
your-bank.co.uk
. In doing so the browser adds theOrigin
header to the request with the valuehttps://your-bank-blog.co.uk
. - The
your-bank.co.uk
server checks theOrigin
header value against the access list. It’s there! - The correct resources are returned from
your-bank.co.uk
along with the headerAccess-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.
- Reflected XSS: When an application employs data from an HTTP request in an unsafe way. The above is an example of reflected XSS.
- 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.
- 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
orhttps
instead ofjavascript
).
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:
<script>alert('hello!')</script>
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.