I gave a talk at ASFWS 2013 about CSRF. The purpose of the talk was to go over an aspect of CSRF which is often neglected.
I stared with an overview of some security engineering concepts: making sure frameworks are safe by default, indicating potentially unsafe features, keeping code easy to audit, surviving copy-pasting, etc.
I then talked about state mutation on read requests (i.e. on GET/HEAD requests). In general, web applications should not mutate state on such requests, there are however many use-cases for doing it.
An important question is: how do you design a web framework which lets you sometimes mutate state on read requests but does not compromise the security of the entire web application?
Here are the notes I gathered while preparing the talk. I got plenty of help from my co-workers, who reviewed my work and engaged in very interesting discussions. Many thanks to Erling, Scott, Mathieu, Keito, Michael, Alec and everyone who helped me!
Cross Site Request Forgery is a web application security flaw which lets a malcious site take actions on a vulernable site's users.
We are interested in any kind of state mutation. Typically, this is going to include:
- databases (SQL, key-value, node-edge, ...).
- also actions which trigger external state change, such as sending an email.
As a web application grows, new caches or backend systems get added. It's useful to keep in mind that the set of systems affected by state mutation can change over time.
Smaller web applications are usually not prone to CSRF on read requests. Depending on the team/codebase size, it's possible to rely on coding practices & code review. Things get ugly as the team & codebase grows. I therefore made the following assumptions:
- We have a web application which exposes a logged-in state over SSL. CSRF attacks on the web application provide some value to malicious attackers (i.e. your bank, web mail or social media sites qualify).
- The team and codebase is large and growing.
- The web application mutates states in various different ways. E.g. different contains different kinds of databases, caches, RPC services, etc.
Traditional CSRF protection
- Traditionally, a CSRF token is embedded as a hidden element in forms which POST data. POST requests which don't contain a valid CSRF token are rejected.
- When applicable, make sure to safely handle other write requests (PUT, DELETE, etc.).
- Make sure to only embed the token if the form is posting back to the same site.
- Be careful with open redirects.
- Be careful with tokens which don't change on every request, compression and ssl.
- The token should be derived from the logged-in state. Simply storing the token in a cookie and comparing it with the POST value is prone to cookie rewrite by a MITM over ssl.
RFC 2616 (HTTP/1.1)
- "[...] the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval. [...]"
- "[...] the user did not request the side-effects, so therefore cannot be held accountable for them. [...]"
- The people who write RFCs are wizards. A lot of work goes into the edge cases, performance, security, etc. I would try to stick to the RFCs as much as possible.
Mixing GET and POST
- This can happen at the data (GET vs POST parameters) or the code level (GET vs POST request handling).
- Some frameworks expose the combination of GET and POST parameters (e.g.
- When $_REQUEST is not available, engineers will simplify things and implement something equivalent.
- The different code path for handling GET vs POST requests usually end up calling common library code.
Modelling state mutation
One way to prevent state mutation on read requests is to have some kind of model/tracking system.
The system needs to be flexible enough to handle the different possible kinds of storage systems.
Legitimate writes on read requests
There is a bunch of reasons for having to write on a read request. Here are some:
- Fetching data from a database and subsequently caching it in a cache layer.
- Writing logs.
- Saving an expensive computation.
- Calling backend services (where the effect of the call isn't clearly exposed).
- Updating inconsistent data (e.g. distributed systems).
- Dealing with tracking pixels.
- Handling links sent in emails.
- Cross domain communications (e.g. oauth).
- Some older mobile devices don't support css. The designers ends up favoring GET over POST.
- and many more... (unfortunately).
Should we deal with this problem at the code layer?
We can whitelist specific write operations:
allow_writes.push(true); writeLogs(); allow_writes.pop();
- Is mostly agnostic of the underlying storage system.
- Is very flexible, so engineers can decide at what layer to whitelist a write.
- Impossible to audit (unless you have powerful flow control tools).
- Can be tricky when writing asynchronous code. E.g. you don't want the global state to leak through closures.
Should we whitelist specific pieces of data?
We can whitelist specific databases, tables or rows?
mysql_config.push("table=logs"); ... mysql.insert("logs", ...remaining SQL query...);
- Easier to audit
- We limit API designs (i.e. can't build richer storage abstractions).
- The API depends on the granularity exposed by the database. I.e. different storage systems will expose a slightly different API.
- The right answer might be a combination of both approaches?
- Worth reading: "Signing Me onto Your Accounts through Facebook and Google: a Traffic-Guided Security Study of Commercially Deployed Single-Sign-On Web Services" by Rui Wang, Shuo CHen and XiaoFeng Wang.
- Also worth reading: Stephen Sclafani's blog, who found some interesting CSRF related flaws!
- When implementing CSRF protection on read requests, it's easy to end up with messy code because of shared global state.
- How much of these problems would be mitigated by data flow analysis?
- Sometimes, it's better (from a performance point of view) to use hidden iframes to load data instead of xhr. You can then read data as it loads. This can lead to issues with CSRF and also click jacking.
- For better cache use & better performance, you should use GET requests for non-state mutating requests.
- What is the right CSRF token length? When does the token need to expire?
- Be careful with CSRF token bruteforce in encrypted + compressed streams. Usually, this happens when combining https and compression at the http layer (zlib).
- Logged out CSRF does matter!
- Defend against MITM over SSL by binding the token to the user's session.
- Don't put CSRF tokens in GET requests: can leak in Referer header, can get bookmarked, more likely to end up in server logs, user might leak the token by copy-pasting it, might end up in publicly viewable proxy logs, etc.
- Should you use a CSRF token per action? per controller? site-wide?
- In the past, GET requests had a max size limit. Libraries would typically use a POST request to perform a "long-GET".
- AppSecForum 2013
- Wikipedia page about CSRF
- OWASP page about CSRF
- Stephen Sclafani's blog
- Signing Me onto Your Accounts through Facebook and Google: a Traffic-Guided Security Study of Commercially Deployed Single-Sign-On Web Services by Rui Wang, Shuo CHen and XiaoFeng Wang
- How can you protect yourself from CRIME, BEAST’s successor?