As a CEO of an outsourcing software development company I've seen a lot of legacy code from previous developers for pretty successful projects which had large user audiences, and I must admit that security very often is the last thing that developers care about.

I believe if someone tells you that you should apply this or that type of protection, you will not make it right unless you hack your (or other) site by yourself first.

In post, I will provide simple examples which will help you to check whether users of your web sites are vulnerable to CSRF attacks. I will start with an old-school example which aim is to understand CSRF issue, and then will end with really important and popular case about hacking XHR/Fetch calls.

Assume your site https://example.com has a form on some page which sends USD from user balance to defined credit card number:

<form action='/send_usd/' method='POST'>
<input name='card_address' type='text'/>
<input name='amount' type='number'/>
<button>Send USD</button>
</form>

CSRF Victim website

When user presses Send USD button, browser will make HTTP POST request with a Content-Type=application/x-www-form-urlencoded header to URL https://example.com/send_usd/ and attach Session or JWT cookie (We assume that site uses Cookies to authorize requests). With this request browser sends a cookie which server set on login request so server understands that user is John Doe. And John Doe has some balance in database which allows to send (withdraw) an amount. Let's imagine that cookie was set for 2 days since login (Or JWT token in cookie is valid for this period).

Now some hacker creates a site https://kittie-images.example/ and places a next form with hidden inputs on home page:

<form action='https://example.com/send_usd/' method='POST'>
<input hidden name='card_address' type='text' value='HACKERS CARD'/>
<input hidden name='amount' type='number' value='1000'/>
<button>Show me the most awesome kittie</button>
</form>

Hackers kittie website

John likes kitties, so at Jan 3d, on his weekend, he googled for Kittie images and found this great website, he does not know it was created by hacker. By the way on Jan 2nd John logged in to https://example.com to check all is good with his money.

When John wanted to see a new kittie he clicked Show me the most awesome kittie button, at this moment, without John knowing, $1000 USD disappeared from his balance at https://example.com.

And the reason – browser attached cookie when he sent request to https://example.com/send_usd/ even for this call which was made from different domain. Thing is that browser stores cookies per domain, and when request is done by <form> then most of browsers attach cookies associated with domain defined in action attribute.

So this is basic Cross-domain attack.

NOTE: I just tested this today in Firefox version 84.0.2 (latest for today) and it worked. Chrome or Safari might block it with features like cross-site tracking protection or defaulting cookies to SameSite=Lax released in early 2020 (see at the end of hint), but it is easy to disable because this feature still brakes some websites. Plus there are a lot of old browser versions on machines without auto-update. Also browsers will never hurry up to fix this due to compatibility reasons.

Self development is always about making right conclusions, so lets do it here:

  • Cross domain attack can't "hack a website". In other words it can't give hacker access to a database or codebase or files on the server, instead it can "hack a user of site", this is important to understand.
  • HTTPS does not save from it at all.
  • Obviously Hecker logged into example.com and analyzing in devtools the requests to understand how to perform attack
  • Attack is always performed from other domain when user should be logged via cookies into victim site in same instance of a browser, and login cookies should not be expired.
  • John does not know that issue was caused by https://kittie-images.example/ website, unless some good guy used Developers Tools on this site to disclosure an attack and left a feedback on place where John can read it.
  • Hacker did not know login and password of the user. Also he can't reed a session/JWT value from cookie, it was just attached by the browser.
  • If site has a vulnerable "change password form", without current password confirmation, then hacker can change a password for a user and then find a username (e.g. for kittie site John used same username when hacker asked him). Then hacker can perform login to victim site having much more features.
  • If target of attack is not John but administrator of example.com which has more power – hacker receives much more opportunities

An advanced facts:

  • If you think moving from cookies to other store like LocalStorage will solve the issue – well, yes, but it will create a bunch of new security issues, which you should know how to handle, and it might be even more work in the end. So better don't start moves unless you understand potential solution. Cookie-based authorizations are not so bad at most points.
  • The site could just have no forms and allow only JavaScript XHR calls by allowing application/json in Content-Type request header. It might be good solutions for APIs and SPA because when browser handles a forms he can always send one of application/x-www-form-urlencoded or multipart/form-data or text/plain, but not application/json. Last one can only be sent from XHR, and it will be protected by SOP and CORS. Source is in spec from whatwg.org. But sometimes you still need to use application/x-www-form-urlencoded or multipart/form-data  values in Content-Type header, for example to transmit files more efficiently to server, even from XHR, and they could be hacked by forms.

So how to protect from it? Implement CSRF.

CSRF implementation

Most of secure websites implement CSRF for all their HTTP requests that change a data on server. Also many CMSs or frameworks like Django implement it for you. By the way security is a very good reason to use framework with CSRF protection when you are not super experienced in such things. Typical messages for CSRF errors in different sites/frameworks:

  • csrf token mismatch
  • an attempt was made to reference a token that does not exist
  • forbidden - csrf token invalid
  • can't verify csrf token authenticity

Such errors mean that site was not able to perform CSRF validation, which happens in some case. But I want to show you typical algorithm.

To implement CSRF you need to generate some unique big word when user logins to a website and set it to cookie along with JWT/session cookie. Then on each request include the same word in input when you render form on backend, or read from document.cookie and attach in another header if you are making XHR form JavaScript. In our example we need to render it to from:

<form action='/send_usd/' method='POST'>
<input hidden name='csrftoken' type='our-long-long-random-word-generated-at-login'/>
<input name='card_address' type='text'/>
<input name='amount' type='number'/>
<button>Send USD</button>
</form>

Then on each call just compare two tokens: one from cookie request header and another from form (or from header if you validating XHR). If they are same – you might be calm that request comes from your domain. Otherwise, send error to user. But don't suspect hacker here, show nice understandable message and ask to refresh a page, sometime it might happen because user just logged in again in another tab, and in current tab you just have an old CSRF token.

😢 Very sad that a lot of sites show "technical" version of this error like csrf token mismatch. Does they think John knows what is CSRF? Why should he know it? Or can't verify csrf token authenticity. Hey, he just logged in on another tab, he should not be aware about some tokens at all, it is your responsibility. Say him at least:

Something went wrong, please try refresh a page

Even instagram thinks that users know what is CSRF:

Image for a hint

Conclusions:

  • Input with CSRF value is hidden so it does not bother the user, in fact user even does not know that you have a CSRF protection unless he opens a Development Tools and analyse Network tab.
  • In case of HTML form on https://example.com hacker can't guess a token which was rendered into form because it was done on user login and he does not know login and password, also he can't read a cookie value itself, he can just make John to attach cookie to request from his browser.
  • If example.com would make an XHR call instead (e.g. SPA), hacker still would not able perform request even if SOP was not saved, because he can't read a CSRF token from cookie. In same time when John sits on example.com JavaScript can successfully read document.cookie (to validate it) as it is his own domain.
🤔 Calling things right: when we and a lot of websites and a lot of frameworks like Django call it csrftoken, some people say that it is bad name. Because CSRF is attack, and aim of this token is anti-CSRF, so better call it anticsrftoken, or csrfprotecttoken

Example of attack when SOP does not save you

I have a detailed POST about CORS and SOP, you can check it here: SOP and CORS like I am 5 .

Here I will show the real example which might be a case for a lot of new websites which use SPAs (React, Vue, Angular) and some APIs on backend.

Imagine developer of example.com now thinks that old-school form is a reason of disappeared money, so he have read somewhere that SPA and REST API is more secure setup for a websites. So instead of CSRF implementation he replaced form submission with next javascript from his SPA:

fetch('https://example.com/api/send_usd/', {
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"card_address": this.address,
"amount": this.amount
}),
}).then(response => response.json()).then( (data) => {
// inform user that money were sent
});

He still uses Session/JWT stored in cookies, and all still works good for John. On backend developer parses a JSON and performs a transaction. Do you think John is safe now?

Hacker sees this change in developer tools and adjusts his form the next:

<form ENCTYPE='text/plain' action='https://example.com/send_usd/' method='POST'>
  <input hidden name='{"card_address":"HACKERS ADDRESS", "amount":"1000", "igoredKey": "a' value='bc"}'>
  <button>Show me the new awesome kittie</button>
</form> 

And clicking button will send POST to https://example.com/send_usd/ with attached Session/JWT cookie and next body:

{"card_address":"HACKERS ADDRESS", "amount":"1000", "igoredKey": "a=bc"}

igoredKey is needed to neutralize equality sign (=) send from form, because text/plain forms are serialized to KEY=VALUE format)

To fix it:

  • Just apply CSRF as explained above
  • Another working and simple technique called Content-Type enforcement. To implement it for all requests on backend check that that value in content-type request header is equal to application/json, otherwise drop a request with any error or exception (if it is your code on yoursite it will send only application/json), so forms can't send text/plain content type. But it is not always possible – sometimes you will still need some multipart content-type requests e.g. to transfer files – because with JSON you can only transfer base64 encoded files, which consumes up to x1.5 transfer size (slower uploads, more traffic). So if you have at least one API endpoint which requires non JSON content-type you need CSRF for it.

Cheap CSRF protection implementation with SameSite=Lax (Alternative to CSRF token)

Instead of tokens implementation you can do much simpler thing, when you set a cookie from backend – add SameSite=Lax attribute to this cookie. You can read more details here: SameSite on developer.mozilla.org

When SameSite is set to Lax value, then cookies will be attached only when requests are made from same origin. Downsids:

  • According to caniuse only 91.97% of browsers support it for this date. So > 8% of your users would still be vulnerable. When anti CSRF tokens or content-type enforcement protects these 8% also.
  • If you are using HTML forms on your website (I hope no), then SameSite=Lax ,might not not work If your forms go to API on another domain e.g. api.example.com which could be the case e.g. for some vendor cloud load balancer, or cloud cost optimisations.

Also browsers plan to include SameSite=Lax by default very soon. For today it is only 66% of browsers (Caniuse defaulting to SameSite=Lax) This means that for today, it is enough to not set SameSite attribute at all and your browsers will not need CSRF protection at all, you can read more Chrome’s SameSite cookie update.

Interesting fact: If you are using fetch to call APIs with application/json Content Type or similar, browser will allow you attach cookies from JavaScript fetch code executed on different origin by ignoring Cookie SamiSite policy even if it is set to Lax. But, here CORS protection is getting activated, so you need to allow Access-Control-Allow-Credentials on backend response headers, and specify credentials: include in fetch call params.

Be secure, be protected and take care about your users! Good luck👍

CSRF Attack