Three C-Words of Web App Security: Part 2 – CSRF

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.

CSRF Classic

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:

  1. Cookies or HTTP Authorization headers are used for authentication and authorization.
  2. The victim has a valid session at the time he/she lands on the malicious page.
  3. 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.
  4. 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

It is also possible to issue CSRF requests via JavaScript on the malicious page. If a flawed CORS policy allows the attacker to set up a malicious page that is an allowed origin, with credentials allowed, then you really have a CORS abuse flaw first and foremost, and you can’t effectively protect against CSRF without addressing it. Part 1 of this series dealt with that, so let’s assume that the malicious page is NOT permitted access by CORS. In fact, let’s assume there’s no CORS policy at all, and Same-Origin Policy (SOP) applies. In this case, any request issued by JavaScript on the page will generate a response that the JavaScript will not be permitted to consume. Just as with classic CSRF, this is a blind attack, and therefore the same basic criteria applies for susceptibility. Additionally, the attack needs to be able to avoid a CORS prefetch, or else the malicious request will never be sent. Remember that pre-flights are NOT sent if all of the following are true:

  1. The method is GET, POST, or HEAD
  2. No custom request headers have been set
  3. The content-type is  text/plain, application/x-www-form-urlencoded, or multipart/form-data

The effect is that if a valid JavaScript request can be created that meets the above criteria, the CORS policy will not prevent the server from handling it as a valid request. Same-Origin Policy will only prevent the JavaScript from reading the response. The XmlHttpRequest’s withCredentials or Fetch’s credentials settings, respectively, will allow the inclusion of cookies or HTTP Authorization headers, just like the classic CSRF requests automatically included.  It’s up to the server-side of the application to identify and reject these requests.

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.

CSRF tokens are one of the most common strategies, often available at the web framework level. These add a random value (often in a hidden form field) to the legitimate page in the application. One version of this implementation is to have the server cache this token. When the server receives a request, it will then check that the token is included and matches what the server has cached. It’s essential that the token is uniquely associated to a specific session, or else the attacker may be able to acquire a valid token to include. Another variant of this is to set the token in a cookie. Even though the CSRF request will include the cookie, this attack will not allow the attacker to read the cookie. As with the first example, the server will expect the body of the request to include the token, and then it will compare the token in the body against the token in the cookie. If they match, it’s treated as a valid request from the legitimate application. This approach alleviates the need to maintain CSRF token state on the server. One more approach, particularly for a web service API, is to have the legitimate application(s) supply the token via custom header. This means every JavaScript request from the client will need to add that header, however it also means that it cannot be supplied without triggering a CORS pre-flight.

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:

  • If you’re expecting a JSON payload, validate that the content-type is set to application/json. This immediately means that regular HTML forms can’t produce a well-formed JSON body, and JavaScript-based requests will be CORS pre-flighted.
  • 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.

To Summarize…

…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.

2 thoughts on “Three C-Words of Web App Security: Part 2 – CSRF”

    1. Mic Whitehorn-Gillam

      That’s a good point. You’re quite correct that they work well when supported. The reason I don’t like to recommend SameSite as an anti-CSRF measure is that unsupported browsers are still widely used, and will just ignore the flag.
      https://caniuse.com/#search=samesite
      I generally lean toward combining more than one solution, and certainly if you know you’re not expecting cross-site requests, using SameSite on your cookies could be one of the solutions. I just feel that in most cases it’s insufficient to trust on its own. Anyone using IE10, for example. would be completely unprotected against CSRF if the SameSite flag was the only defense.

Leave a Comment

Your email address will not be published. Required fields are marked *