The title of this post is a reference to the Everybody hates Chris TV show, which felt punny and suitable as I ran into some interesting problems in the last few weeks having to do with CSRF.
What happens when everybody hates CSRF? They put a safety check for it.
When I say "everybody" I'm talking about the maintainers of frameworks/libraries. They don't want CSRF vulnerabilities in their software and none of us want our dependencies to have any either.
I believe this is the story of one odd exception to that default configuration though.
Cross Site Request Forgery (CSRF)
There's a bunch of great resources out there that explain what CSRF is and the security measures required to mitigate it.
CORS, XSS and CSRF with examples in 10 minutes
Aleksandar Maletic ・ Dec 23 '19
Defense Against the Dark Arts: CSRF Attacks
Richard Feldman ・ Apr 17 '17
OAuth Provider Authentication in Rails
The CSRF problem I'm going to talk about is for OAuth Provider Authentication. The specific references I'll explain and link to are Rails libraries/gems, but the concepts should apply similarly across different languages/frameworks.
The process is mostly standardized. When implementing a 3rd party OAuth service you redirect your users to their website and they will make a callback request to your site with a success/failure result.
The reason I'm using Twitter and Apple as the examples above is because Sign in with Apple (SIWA for short) does things a bit different, like performing the callback redirect with a POST instead of GET request.
A great article that goes in detail about the intricacies that make supporting SIWA tricky is the following (explains lots of the same concepts I'm covering in this post):
"Sign in with Apple" implementation hurdles
Adam Coster ・ Oct 10 '19
For our particular case (Rails) we can say the problem with it is that Rails goes the extra mile with CSRF on POST requests. But also a few gems in the stack include checks of their own for CSRF that need to be explicitly disabled.
Rails CSRF countermeasures
Here's a simplified diagram of the stack in use:
Since everybody hates CSRF almost all of them add their own validations:
- Action Pack checks the ORIGIN header of POST requests so they are required to match the website's own domain, i.e.
request.base_url
(source here) - Omniauth-OAuth checks for a
state
value sent in with the request that should be available within the session when the callback is performed (source here) - omniauth-apple validates a
nonce
set in the session that needs to be available (source here)
Making it work
When Apple makes their request back to our website we have to bypass protect_from_forgery
on that specific callback, otherwise Action Pack will raise a CSRF exception because the HTTP ORIGIN header won't match (it will arrive as https://appleid.apple.com
).
A good way to do this would be to bypass this check only on the callback request path and also only for the https://appleid.apple.com
ORIGIN. I've referred to this with the analogy: Leaving your front door open for anyone vs only giving Apple the keys to unlock the door.
The problem with the two other checks is that the validations are checked against values in the session
. Sessions are managed by cookies and without the cookies those values won't be available. Why is this an issue?
SameSite=None Cookies
We've run into some issues where the cookie that manages our session isn't available, therefore breaking checks 2 & 3 mentioned above. If interested, this issue has the most in-detail conversation on that problem specific to our case.
We don't want to compromise our Cookies to have SameSite=None
enabled, so we're currently testing to make sure that SameSite=Lax
work good enough in production environments.
This is a great read on SameSite cookies explained that dives deep in the topic. Here's one of the diagrams they use to explain the concept and how browsers are shifting away from defaulting to SameSite=None
What does this mean for SIWA? Is it still safe after disabling all these security checks?
I believe so, because the payload arrives in a JSON Web Token (JWT). This JWT is signed using a private key that needs to be setup in the Apple Developer Portal which is available only to your backend (source here and here).
This payload comes in the POST request so it should be secure and trustworthy. In this case, the fact that there are multiple checks spread throughout the stack make it counterintuitive. I mean, all those checks are there for a reason, right? It's a good default to have but tricky to deal with in some cases.
What do you think? Is it safe to bypass all these checks? Has anyone had a similar problem using other frameworks?
Top comments (3)
Sometimes I'm not sure what's worse - a CSRF attack or the convoluted mess of a protocol that is CORS with its preflight OPTION calls and allow-origin headers - good grief. Oh the hours wasted...
JWTs are signed, that means you can be (reasonably) certain that nobody tinkered with their content. However, they are not encrypted (just base64 encoded), so they do NOT ensure that a third party attacker can't read their content and steal information this way. If you use JWT for authentication, set a suitable time-to-live. In combination with HTTPS/SSL that should guard you against most basic attacks. I'm not a security expert though.
I think it's great to openly talk about these topics and specially in approachable ways for non-security experts, like myself too. If we're going to disable security checks it's important to understand the consequences/risks. It makes you second guess yourself when it's everyone but you that wants these checks in place 😅
💯
True that they're not encrypted, but the JWT travels through the HTTPS request so I think I'm comfortable with that. I believe the choice of using POST for the callback by the teams at Apple was for a reason (not "just because"). I just don't know enough about the vulnerabilities in these protocols to explain why.
If only they wrote documentation about it :-P