This is the second in a three-part series, Three C-Words of Web Application Security. I wrote a sort of prologue back in April, called A Brief Evolution of Web Apps, just to set the scene for those less versed in web application history. In July, I posted part one, which was Three C-Words of Web App Security: Part 1 – CORS. This one will deal with CSRF (Cross-Site Request Forgery), while the final post in the series will address Clickjacking, and the role it can play in CSRF exploitation.
In Part 1, I identified several types of requests as generally considered safer, and therefore not subject to CORS when being sent across origins. Some examples were the HTTP GET requests that the browser automatically makes when the HTML engine parses a tag that requires another resource, like a <script> tag or an <img> tag. A regular HTML <form> submit is another example of this. In these examples, one thing to note is that these types of requests don’t typically have a risk of exposing anything sensitive to the page. For example, if a <form> on secureideas.com is posted to professionallyevil.com by the normal submit event, the response is never returned to a context where scripts on secureideas.com could read it. The browser would effectively navigate to professionallyevil.com to show the response. All these examples are common, everyday cross-site requests which are perfectly fine. One significant thing to note is that if the browser has any cookies or HTTP Authorization headers for the target site, it will automatically include them in cross-site requests.
So in the previous example, if the person browsing had previously logged into professionallyevil.com, they may have received a SESSION_ID cookie. This cookie is scoped to professionallyevil.com, so when they browse to secureideas.com, the browser will not allow the page on secureideas.com to read it. However, if the <form> on the secureideas.com gets submitted to professionallyevil.com, that SESSION_ID cookie will automatically be sent as part of the request. This is normal, acceptable behavior for the browser, and is critical to how web content works. But, maybe you already see where it presents a problem from a security perspective.
That SESSION_ID is how professionallyevil.com identifies that the request is coming from an authenticated person, and who that person is. Cross-site request forgery (CSRF) is malicious exploitation of this particular behavior. An attacker can set up a malicious site that uses regular HTTP interactions, such as GET and POST actions from an HTML form, to send authenticated requests to a vulnerable application. This can be done in a hidden way without any user interaction beyond navigating to the attacker’s page. The prerequisites for such an attack are as follows:
- Cookies or HTTP Authorization headers are used for authentication and authorization.
- The victim has a valid session at the time he/she lands on the malicious page.
- All of the other parameters for the request are known to the attacker. The attack site could also present a ruse to trick the user into entering some of the parameters for the request, but if it’s something the user wouldn’t typically supply outside of the target app, one could argue that does not demonstrate more risk than regular credential phishing.
- The attack is effective even though the attacker is unable to see the response. This means that it needs to mutate data or application state in some way that is harmful, such as changing the victim account’s associated email address on the target app.
Hiding the attack is simple. The whole form element can be hidden using CSS, and could actually be submitted through a hidden <iframe> element to keep the user from being navigated from the malicious site.
CSRF via XHR/Fetch
- The method is GET, POST, or HEAD
- No custom request headers have been set
- The content-type is text/plain, application/x-www-form-urlencoded, or multipart/form-data
Defending Against CSRF
There are a few strategies to defend against the attack, generally with the goal of ensuring only the requests originating from the actual legitimate application are accepted.
Credential re-prompts effectively defeat the CSRF attack as well. Consider the common case of requiring the current password when assigning a new password. Because the attacker does not known the current password (if they did, they wouldn’t need the CSRF attack), they can’t supply that as part of the request.
Origin and Referrer checking is yet another strategy. As I outlined in the CORS post, the browser will automatically include an Origin header (and usually referrer header, although there are some ways of manipulating it) on cross-site requests. While a custom script can easily spoof the Origin, validating these headers as matching the legitimate application is enough to solve the actual attack scenario since it required cookies/auth set in the victim’s browser. A bonus is that this strategy can often be built-into your CORS logic. If it’s a cross-origin request and the origin is not allowed (e.g. you would not reflect it in you access-control-allow-origin header), reject the request.
Out-of-Band required fields like a CAPTCHA can effectively act like a CSRF token since they require input that is unknown to the attacker.
Multi-stage transactions are more difficult to attack via CSRF. In particular, if there are multiple requests required and each one requires value(s) from the response to the previous request, the attacker has no means of obtaining those values. For example, if you fill out the create a user form, and you are directed to a confirmation page that prompts: are you sure you want to create this user? [y/n], and that confirmation form has an unpredictable transaction_id that the attacker wouldn’t know to be able to supply.
Best practices mitigate some of the risk as well:
- HTTP Get requests often can’t practically have CSRF protection. If they follow the best practice of not mutating data or application state, they’re inherently not susceptible to CSRF.
…here are some things to look at whether you’re implementing a secure solution or validating the implementation in the context of a penetration test:
- Are there CSRF-sensitive transactions? Specifically from the attacker perspective, I need to find something that would be damaging if done blindly. Some of the more common things that can lead to a bigger compromise are changing a user’s password or associated email address, but inserting, updating, or deleting any piece of data may have serious effects depending on the specific context.
- Is the auth mechanism CSRF-sensitive? Is it a cookie or an HTTP Authorization header? Or is it a custom header, which inherently resists CSRF as long as the CORS policy is set correctly?
- Is there visible anti-CSRF protection? Most, but not all of the defenses have some observable element, such as tokens in some part of the responses (headers, body). More often than not, if I can’t see the protection, it isn’t there.
- If I delete a CSRF token, is it actually enforced? It seems like a silly question to ask, but every month or two we seem to see an application where there are CSRF tokens present, but the application doesn’t actually check if they’re present in the request.
- Is the CSRF token tied to a specific session? If I switch from User A to User B, can User B use User A‘s token? As an attacker, could I create a script that gets a token to use in a CSRF request?
- Does the password change actually require the current password? Just because the field is present doesn’t mean it’s properly enforced. If I remove that parameter, will the request be rejected? What if I set it to a boolean true?
- Is the content-type of application/json required? Will, for example, text/plain be accepted and parsed as JSON?
- Does the confirmation page actually send a value that an attacker wouldn’t know? If not, the attacker might be able to send two blind CSRF requests to beat the confirmation.
- Is there origin checking beyond setting the CORS policy?
- Similarly to CORS, are there other origins in-scope that might be weaker targets that are allowed to send request? That old unpatched WordPress instance hosting the company blog, perhaps?
- Are there any GET requests mutating the state? How are they protected against CSRF? Most solutions won’t work well for GET requests, but modifying the URLs to include a token or similar at render time (whether that be client or server) can work. The best choice is to follow the best practice of avoiding mutating state in GETs.