4 ms·
No. - You visit evil.com - Evil tries to make an HTTP request to bank.com/transfer.php - The browser happily performs the request, authenticated with your co
by tgsovlerkhgsel 7y ago
No.
- You visit evil.com
- Evil tries to make an HTTP request to bank.com/transfer.php
- The browser happily performs the request, authenticated with your cookies, and the bank, having a CSRF vulnerability, happily sends your money to the attacker.
- Since 'evil.com' and 'bank.com' are different origins, Browser refuses to provide the response to evil.com, but the attacker doesn't care, he got the money.
CORS allows you to relax these restrictions, not tighten them.
Now, bank doesn't like these attacks. So they make the legitimate application send an additional custom header, "X-Totally-Secure: true". Despite being a really bad idea, if (big if) the browser follows the standards, this prevents the attack:
- evil.com tries to make the HTTP request as before
- Browser lets it through, as before
- Bank rejects the request because it's missing the magic header
So the attacker adds the header to the request:
- evil.com tries to make a non-standard HTTP request to bank.com/transfer.php, with the header attached
- BECAUSE IT'S A NON-STANDARD REQUEST, browser asks bank.com (as you described, OPTIONS)
- Bank.com replies "wtf do you want I don't know what OPTIONS is"
- Browser refuses to make the request
Unfortunately, the bank forgot that they have a marketing department, that runs ournewbankapp.com, and shows your current balance in the fake screenshot of the app to show how awesome it is. And your bosses' bosses' boss has yelled at the IT department that rolled out the security measure to make it work again. They make ournewbankapp.com send the magic header (including access-control-allow-credentials), but now the OPTIONS request fails. So they teach the web server to respond with "everyone is allowed" (with "access-control-allow-origin: *" as you described) because they're lazy and dumb.
But because browser vendors know that developers are lazy and dumb, the browser completely ignores this: If access-control-allow-credentials is set, the allowed origin must be listed explicitly. The developers give in, and explicitly add ournewbankapp.com to the header, and now it works, but the attack doesn't work.
(part 2 follows)
- superasn 7y agoIt certainly won't allow me to make xhr POST requests without sending an OPTIONS request first. Are you sure about that?
- giaour 7y agoYes, you can send some POST requests without a preflight OPTIONS request: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simpl...
- tgsovlerkhgsel 7y agoDon't trust, verify: https://news.ycombinator.com/item?id=20406633 https://news.ycombinator.com/item?id=20406633
- superasn 7y agoAmazing. You learn something new everyday. Though it does make sense why blocking it won't serve any purpose as form submissions are going thru anyway. Also glad I didn't mention jsonp. God knows what the devs at ournewbankingapp would do with that one ;)
- aaronharnly 7y agoI know you know this, but to clarify steps 2/3: - Evil tries to make an HTTP request to bank.com/transfer.php If this is a regular HTTP POST (ie submitting a form, and the browser window changes location), the browser will allow it. If this is an xhr POST, the browser, following the same-origin policy, allows the request, but prevents accessing the response (unless allowed with CORS). [EDITED with tgsovlerkhgsel's help!]
- metafunctor 7y agoHow hard is it to trigger a regular HTTP POST instead of an xhr POST?
- Thorrez 7y agoNot hard. Just make an html form with the values you want, then with javascript call document.getElementById("myForm").submit();
- tgsovlerkhgsel 7y agoEven a JavaScript initiated POST request will go through. Blocking it would not make a lot of sense, because the attacker could just use the FORM (possibly in an iframe to keep it invisible to the victim). It is possible that XHR, or common XHR libraries, default to adding some header that makes it a non-standard request, but a fetch() call works. In Firefox, open a debug console and run fetch('https://otherorigin.example.com', {method: 'POST', body: 'blah'}) You will see two things. In the console: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://otherorigin.example.com/. (Reason: CORS request did not succeed). In the network tab, a HTTP request. Replace with a URL of a server you control, or run `nc -lnvp 9999` and replace the URL with http://127.0.0.1:9999 http://127.0.0.1:9999, to see that the request is indeed being made. As the author said... few people understand CORS.
- aaronharnly 7y agoThank you, I stand corrected! Editing :-)
- tgsovlerkhgsel 7y agoWays this story could have ended badly for the developers: - instead of hardcoding the allowed value, they set it to always echo the value of the Origin header - browsers are powerless against that much stupidity, and can't distinguish it from a correctly configured server that is allowing the request. evil.com sends the request, bank says "evil.com" is allowed, browser shrugs and sends the request. - instead of using CORS, they could have tried to build their own custom solution that allows ournewbankapp.com. The attacker would have to analyze how it works, and would then most likely find a way to exploit it to perform the attack, while legitimate users with privacy extensions would make the support hotline despair due to countless people complaining "I can't send money". - instead of adding the custom header, they could have decided to check if "origin" is present, and if so, assume it's a cross-site request and check the origin against a whitelist. This still relies on standard-compliant browsers but isn't the worst idea AS LONG AS YOU REQUIRE AN ORIGIN VALUE, AND TREAT LACK OF AN ORIGIN HEADER AS A FATAL ERROR, BEFORE TAKING ANY ACTION (e.g. the money transfer) BASED ON THE REQUEST. I think can force same-origin requests to include the origin header with some option on fetch(). But if you treat a missing (or "null") header as ok, the attacker will likely find a way to make some browsers send no header, and steal money again. Sending "null" (or some other default value, been a while since I played with it) is easy, I think with some iframe trickery. - they could have used plain HTTP, exposing them to DNS rebinding attacks even from remote attackers who cannot sniff their user's traffic. - they could have an XSS vulnerability somewhere on the main site or the whitelisted marketing site, allowing the attacker to proxy his malicious requests through that site (and thus make them use a whitelisted origin). Maybe some long-forgotten kludge designed to proxy requests to third parties that don't support CORS...
- aaronharnly 7y agoVariant of #1 that I've seen not infrequently: They set it to check a regex for `bank.com` to also allow `subdomain.bank.com`, but inadvertently also allow `bank.com.evil.com`, `evilbank.com`, or similar.
- Thorrez 7y agoHow would DNS rebinding work? I thought DNS rebinding only works if there's some sort of authority that's ambient across hostnames (such as whitelisted user IP ranges, or connectivity to a network), which doesn't seem to apply here. If you do have authority that's ambient across hostnames, my thought of how to protect yourself is to check the Host header. XSS is essentially completely separate from CORS and CSRF. XSS is a bad vulnerability, worse than CSRF, and you can't expect any type of CSRF prevention to protect against XSS.
- quickthrower2 7y agoAnother option is to use a secure token (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md https://github.com/OWASP/CheatSheetSeries/blob/master/cheats...) this is a PITA but frameworks like ASP.NET have this baked in so it's not really much of a hassle in practice.
- jchw 7y agoActually they can do better than just a custom header with an XSRF token, which is fairly standard nowadays for requests that are authenticated via Session Cookies (Django for example strongly enforces this by default.)
- tgsovlerkhgsel 7y agoIndeed, this is the canonical way of avoiding CSRF (= XSRF) attacks. I intentionally only explained the header method because I didn't want to confuse readers with another concept, and because it's a really nice example to explain CORS. https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md https://github.com/OWASP/CheatSheetSeries/blob/master/cheats... is probably one of the best documents discussing this, including the drawbacks of the header method that I used (and warned against) in my example: https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md#use-of-custom-request-headers https://github.com/OWASP/CheatSheetSeries/blob/master/cheats...
- strictfp 7y agoWait. If the resource at bank.com is protected by a CORS filter, the request which is issued at step three will have an origin header, and the CORS implementation is free to issue a 401 at that point. At least as far as I have understood. That would be tightening the restrictions. Am I missing something?
- tgsovlerkhgsel 7y agoThere used to be ways to get rid of the origin header, not sure if this is still the case. You can easily set it to "null", e.g. with some iframe trickery.
- MrBuddyCasino 7y agoThis is the first comprehensible, and more importantly memoizable, description of CORS I have read. Hope it sticks this time.
- phdsolutions 7y agoAs others have mentioned - thanks for this! I thought I understood CORS, but one thing I apparently wasn't clear on was the conditions on which a pre-flight request was made. Thanks for clarifying!