Before we get into the meat and potatoes of this post, we need to take a look at a couple of concepts in appsec. The first one deals with Cross-Origin Resource Sharing (CORS). This is a complex security mechanism that is built into all modern browsers. If you are not familiar with what it is and how it works, you should read this very good article on it. The second concept deals with HttpOnly cookies. If unfamiliar, you can read up on that here.
For this exercise we’re going to use an app/API that I described in a previous blog post here as an example. To paint this picture we will assume the following:
1) We have an endpoint that properly validates session cookies.
2) Session cookies are properly protected with httponly and secure flags
3) CSRF tokens are not used by the app
4) Full collection of server headers are as follows:
Wrong. There appears to be an exception to the client-side script access prohibition for XMLHttpRequest that will still allow you to access the session cookie even with the HttpOnly flag set as long as the withCredentials property is set to true. This means that we can still execute CSRF using a victims session cookie that is properly secured with the HttpOnly flag.
Of CORS There’s Always A Catch
If you looked at the article on CORS that I linked in the background section, you will see that we have another hurdle to this CSRF execution. CORS is a complicated security concept that is the bane of many a web developer. It’s also the bane of many an attacker. When I first attempted to execute the CSRF on the web app, I kept on seeing the below error in the console when I tested my POC. “Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource…”.
Since the victim server doesn’t have the Access-Control-Allow-Origin header set, it looks as though we’re stuck. Well, sort of. As it turns out, we still send our request to the server AND the request is still executed on the server using the secured session cookie! The error implies that the request was blocked by the browser. However, upon closer examination we see that it’s only the response that the browser discards. What does this mean?
Putting It All Together
Let’s use this PHP as the server example of simulating an update to user information. I’ll link the victim and attacker code in full at the end of this post so you can play with this yourself.
Let’s say that we have an admin user that logs in to the application then receives a phishing email with a link to our malicious server. When the admin navigates to the malicious server, an XMLHttpRequest is executed against the victim server that updates the adminId 1 with the attacker’s email address and a password of their choosing.
Now as a result of CORS restrictions we’ll end up with an error, but the error is generated by the browser itself. NOT the server. This means that the server received the request and faithfully executed it complete with expected response. However, we’re unable to read the response. So as long as we aren’t counting on the data in the response like in this particular scenario we can still execute CSRF. Even with HttpOnly and CORS protections in place.
As it happens, this gap has been noticed and a fix is in. There’s a cookie flag called SameSite. This is a relatively new flag and Mozilla lists it as experimental. Additionally, PHP as of 7.3 includes this capability.
However, this might present some design hurdles with existing web apps that should be taken into consideration when reviewing the implementation of this flag.