
Securing your API: a modern alternative to CSRF tokens - skeggse
https://mixmax.com/blog/modern-csrf
======
ejcx
Why make an extremely complicated set up, with many edge cases, all to save
yourself from a single token?

    
    
        - Privacy extensions often times block referrer headers.
    
        - POST requests are usually necessary.
    
        - Open redirects are common bugs, and getting your website to initiate one can be a problem.
    
        - Disabling CORS also relies on you killing crossdomain.xml, which you might overlook.
    

Instead you can just roll out CSRF tokens.

Having rolled them out myself many times. You probably want to use a library
if you aren't a cryptographer or security person:

    
    
        expiration_time_of_1_day || hmac_sha256( secret_key, { expiration_time_of_1_day, user_id })
    

I've seen other people mention SameSite cookies, but we aren't near a time
when browsers all support them. Don't get fancy preventing CSRF. It's a stupid
bug.

~~~
ubernostrum
Not to be That Guy, but: your sample will reuse the same token over and over
until it expires, which is how you get the BREACH attack.

In Django 1.10, we switched the CSRF token generation to use a consistent
_base_ value, but to combine it in a reversible way with a randomly-generated
per-request nonce. CSRF verification then consists of recovering the base
value and checking it; this lets you have a longer-lived "token" without
sending the same value in every request/response cycle.

~~~
ejcx
You're wrong. If this is generated every time the page loads you get a new
token each time. OR you have to transmit a god awful amount of data on the
same page.

When I work for a company that accepts GBs of data over the same TLS
connection on a single page, I'll worry about breach and only generating a
CSRF token every time a page loads.

~~~
ubernostrum
From the sample code provided I was assuming the value would not update on
every request (since, if you were changing the expiration timestamp on every
request you'd have a bit more work to do than you've shown here -- and either
way the sample you've provided isn't a great way to generate the token).

~~~
ejcx
I've generated this exact token at several companies and it's worked great.

An expiration of one day is not 86400 it's unix_time()+86400, to clarify. It's
not stored in the users session and is completely stateless. It's great!

It has every property a csrf token needs

\- per user

\- not guessable or forgeable

\- expires

Since it is stateless it doesn't require db lookups which can be an additional
benefit. If you complain that it lacks the ability to be revoked I challenge
you to find a single instance of people revoking csrf tokens.

------
arkadiyt
I used to prefer the origin/referer approach to blocking CSRF because it can
be done upstream of the application server (or in a middleware), transparent
to developers who often get these things wrong.

However there's been enough referer spoofing browser bugs lately that I'd
rather have the extra safety (and complexity) of CSRF tokens. Just 3 months
ago Edge had (another) referer spoofing bug:
[https://www.brokenbrowser.com/referer-spoofing-patch-
bypass/](https://www.brokenbrowser.com/referer-spoofing-patch-bypass/)

~~~
Eridrus
And another one on slide 33:
[https://speakerdeck.com/filedescriptor/exploiting-the-
unexpl...](https://speakerdeck.com/filedescriptor/exploiting-the-
unexploitable-with-lesser-known-browser-tricks)

There are plenty of middleware-like solutions that can add solutions
automatically too. And if you fail closed, you will always notice in testing
and can add tokens where they are missing.

~~~
wearhere
Interesting. But in both these cases (yours and @arkadiyt's) these
vulnerabilities only affect GET requests right? In which case—though we would
love to lock down GET requests, to prevent DOS attacks, and because GET routes
_might_ in some cases modify state—the impact is pretty limited.

I (one of the co-authors of the post) would also characterize our approach as
"skating to where the puck will be". I'm sure that browsers will patch these
bugs, the Edge one was fixed quickly. Our product only nominally supports the
latest - 1 versions of Chrome and Safari. This is of course a luxury not
available to all developers.

~~~
Eridrus
Almost no one ever gets hacked through web app bugs at all, let alone CSRF, so
in the end this doesn't really matter.

I still wouldn't recommend this as a solution though, since it's been broken
repeatedly.

~~~
616c
Man oh man I used to think the same but I took infosec training.

That taught me little. I held onto the belief hacking a target by website is
only for really dumb victims.

Then I started reading Bug Bounty reports on HackerOne and BugCrowd and was
terrified at people doing account takeovers with CSRF attacks on oft
overlooked functionality in no name sites like Twitter or FB.

That humbled me real quick.

As an aside, FB has to put a stupid interactive prompt to not open the browser
JS console unless you type a key code. In an era where people will misguidedly
copy paste into that, it is real brazen to believe token stealing and other
CSRF vectors are not footholds and do not really happen.

I presume I just misunderstood you, but I wanted to go on the record for those
on HN secretly believing this position and call them nuts.

~~~
Eridrus
Are you FB or Twitter or on a similar scale?

Also, bug bounties are not representative of real attacks.

I've written my own share of complicated exploits, but from an actual defense
perspective... that's not how people are getting hacked IRL. It's all word
macros and sqlmap.

------
minus7
I can't shed the feeling that CORS is just piling more crap onto crap; this
seems just way too complex. On one hand, I'm somewhat glad that I don't work
on web things and don't have to deal with it, and on the other hand I'm scared
of the many web developers are not (sufficiently) aware of the range of
possible vulnerabilities and thus not protecting them.

------
codedokode
They tried to propose a simple modern solution but after reading the article
it does not look simple and works much worse than traditional approach with
tokens.

And of course it would be better if browsers would not make cross-origin
requests unless permitted by server.

~~~
wearhere
Hi @codedokode, I'm one of the authors of the post. There's a lot of
background material at the top but if you skip to the use of our new module
(direct link: [https://github.com/mixmaxhq/cors-
gate/#usage](https://github.com/mixmaxhq/cors-gate/#usage)) I think you'll
find it simpler in both code and infrastructure than a typical CSRF setup.
[https://github.com/expressjs/csurf](https://github.com/expressjs/csurf), for
instance, requires you to lock down every API both server-side and client-
side, and by default requires session middleware; whereas with cors-gate you
can register it once, server-side, before any API routes.

------
lol768
I guess the modern part here is the use of the Origin header, but in all
honestly this feels really flaky in comparison to using a CSRF token. I'd
argue that SameSite cookies are a better 'modern' alternative than this (for a
majority of use-cases), especially because of all the edge cases that have to
be dealt with for this approach (Origin not being sent, falling back to
Referers, policies for those...). It feels like a giant hack.

Also, I really don't get the relevance of the refutation of "Use only JSON
APIs" \- it's a fixed bug that seemingly only impacted Chrome anyway? The
second point is that preflights are expensive, which might have some weight -
but e.g. Twitch seem to cope okay with these preflights and I think a lot of
browsers do cache them for a short period of time now anyway.

I'll be sticking to CSRF tokens plus SameSite cookies (for where the browser
supports them). The only issue I can see with SameSite is that JS can still
send requests (with no credentials), but I'm not convinced this is a credible
DoS vector.

------
rossy
I do this in one of our business' apps. It does strict checking of the Origin
and Referer headers when it receives a POST request. We only support modern
browsers, and doing this instead of using CSRF tokens feels modern and clean.

I'm paranoid though, so at the last minute, before shipping the first public
version of the app, I added a traditional CSRF token check in addition to the
Origin/Referer check. I guess it's a layered defense?

------
alexchamberlain
I don't really like the word secure next to CORS; you're relying on the
browser to be secure... which it is not.

~~~
wearhere
You can trust the browser itself (not application code, but the native code)
to issue the appropriate headers. If you could find a way of compromising
that, it would be a vulnerability wayyy beyond the scope of our protection.

(Caveat: browsers may not implement the specs completely/bug-free yet, as we
cover in our post. But we fully expect they will, and in the meantime our
module supports fallbacks. This approach is "skating to where the puck will
be".)

Non-browser clients can spoof these headers, but the risk then is DOS, not
clients leveraging the user's credentials—which is the primary focus of CSRF
protection. It's nice that our method can prevent browser-based DOS attacks,
but that's by no means complete DOS protection.

~~~
ubernostrum
_If you could find a way of compromising that_

CVE-2011-0696 (the Django version of a bug that _did_ affect several major
things) is what happens when you find a way of getting the browser to make a
cross-domain request with custom headers.

(the underlying issue there was a combination of a bug in Flash, and the
semantics of the HTTP 307 status code)

------
alexlongterm
On the same topic of CSRF prevention, we also recently wrote about the dangers
of using Content Types for security --
[https://medium.com/@longtermsec/chrome-just-hardened-the-
nav...](https://medium.com/@longtermsec/chrome-just-hardened-the-navigator-
beacon-api-against-cross-site-request-forgery-csrf-690239ccccf)

------
hamandcheese
> Firefox will not send the Origin header with any same-origin requests (bug)

Am I the only one very surprised to hear this? In 2017?

Is there any reason browsers shouldn't just send origin headers along with all
requests? Why the exceptions?

~~~
skeggse
Well, it's a bug. It absolutely should send the origin header, but it doesn't
¯\\_(ツ)_/¯

~~~
hamandcheese
I see that it's unclear, but my second set of questions was related to how
chrome and safari don't send it for requests caused by img tags.

------
Retr0spectrum
The author admits that this solution won't work for "older" browsers. Which
browser versions specifically?

~~~
nevir
[https://caniuse.com/#feat=cors](https://caniuse.com/#feat=cors) and hit "show
all"

TL;DR - evergreen browsers, mobile browsers (except for images), IE 11+, and
IE 8+ partially supports it

~~~
skeggse
For our specific solution, we also rely on (partial) support for Referrer-
Policy: [https://caniuse.com/#feat=referrer-
policy](https://caniuse.com/#feat=referrer-policy)

------
ziwikiwi
What a well-written, insightful article! You have outdone yourself good sir

------
makkesk8
What stops a user from writing a simple HTTP client to spoof the header?

~~~
treve
Then that user can only 'hack themselves'. CSRF is a security vulnerability is
because you can do a HTTP request on behalf of someone else, on someone else's
browser, in someone else's session/security context.

------
mrmagooey
Isn't JWT a modern alternative to CSRF tokens?

~~~
vmasto
It's not. If you think it is you probably store JWT unsafely instead of in an
httpOnly secure cookie.

~~~
hawkweed
Why do you think storing JWT in secure cookie is only secure solution?

------
homakov
If I were to drop CSRF tokens (which work reliably) I would go directly to
Origin verification. No Referer. Something in the middle is worst of both
worlds.

~~~
skeggse
I'm curious about your comment. We (attempted) to address cases where we
needed to infer the Origin from the Referer due to incomplete browser support.
What about using both makes this necessarily worse, when the use of the
Referer is really only a temporary bandage for said incomplete support?

~~~
homakov
Worst because it is neither proven as tokens which are used for decades
already nor as convenient as simply whitelisted Origin and you even offer an
extra dependency. Are those scenarios without Origin important? Like ff for
form request - you can use xhr there and drop referer support entirely.

~~~
skeggse
You're right, support for cross-origin form POSTs isn't important for us. What
is important, however, is same-origin requests, and Firefox doesn't send the
Origin header with those. For us, the Referer serves its most important role
by indicating where those requests originate from.

 __EDIT __: when Firefox _does_ start sending the Origin header, we may drop
the Referer - we 'll see.

~~~
homakov
Just checked, indeed no origin on same site. It sucks, this makes origin
barely reliable.

------
bradavogel
Thoughtful article!

~~~
janwillemb
Thus said the "Co-founder & CTO @Mixmax" ;)

~~~
bradavogel
:-D

