Every several years, a boring old problem becomes relevant again, typically because those who develop new technologies fail to learn lessons from the mistakes of those who developed old ones.
This is apparently the case with Cross-Site Request Forgery (CSRF) and GraphQL server technologies.
In REST APIs, state-changing operations are explicitly linked to certain HTTP verbs: creation is performed with POST, updated with PUT or PATCH, and destructed with DELETE.
GraphQL also clearly distinguishes between operations used to fetch/read data (queries) and state-changing operations (mutations), which should be executed exclusively using POST requests.
One could argue that GraphQL reduces the likelihood of state-changing operations being intentionally implemented using GET requests … but it turns out, GraphQL servers are still just as likely to be vulnerable to accidental misconfiguration.
As an example, in November 2019, GitHub found that faulty validation logic in their Ruby-on-Rails-based application resulted in HEAD requests being handled as authenticated POST requests, effectively bypassing their OAuth controls.
Less than two years later, a HackerOne report disclosed how an attacker could bypass Gitlab’s X-CSRF-Token header validation in the specific GraphQL mutations by using the GET requests: “When we send a GET request, the backend does not expect the X-CSRF-Token header. Using this, an attacker could leverage this to bypass the existing CSRF protection”.
So, HTTP Method/Verb Confusion, once old, becomes new again.
On the other hand, most GraphQL APIs use POST requests for all their state-changing operations; however, they are still vulnerable to CSRF under certain conditions.
Assuming that there is no effective anti-CSRF mechanism in place, an attacker can leverage certain content type misconfigurations in order to exploit CSRF.
From the docs:
By design, GraphQL APIs should only accept POST requests that use the application/json content type.
But it’s not uncommon for attackers to send seemingly unsupported data formats with the right number of middleware-related shenanigans.
Again, this is nothing new, it’s a well-known issue in REST APIs, and it’s very much worth checking in GraphQL.
BatchQL is a “security auditing script” that tests some of the scenarios described in this article, as shown below:
- GET based CSRF
- POST based CSRF
As the tool’s documentation explains, this is not “a foolproof way to determine whether a GraphQL API is vulnerable to CSRF attacks; however, it is a good indicator to perform further analysis to confirm if CSRF is possible.”
Ok, so how do we learn from the mistakes of the past?
We don’t need to re-invent the wheel. Luckily, the technical part was figured out long ago.
We need to ensure that the GraphQL servers are secure by default, and securing applications against CSRF is solely the developers’ responsibility.
Out of the box, servers should only allow mutations to be executed if they’re sent via POST requests, while POST requests should only be accepted if they use the application/json content type.
Since developers sometimes mistakenly implement state-changing operations as queries, servers should also strongly encourage (if not simply enforce) queries to be implemented via POST.
Additionally, servers should help developers understand the impact of their CORS and cookie configurations. Good documentation and secure default options go a long way, as well as alerting users whenever they make dangerous decisions, such as accepting incoming requests from all origins (Access-Control-Allow-Origin: *) or setting their cookies with the SameSite=None directive.
Finally, these servers must support and adopt strong anti-CSRF mechanisms, such as Anti-Forgery Tokens, by default.