# That single GraphQL issue that you keep missing

GraphQL endpoints accepting form-urlencoded or GET requests enable CSRF and XS-Search, letting attackers force authenticated users to run queries and mutations.

- Payout: Research
- Severity: medium
- Program: GraphQL
- Write-up by: Doyensec
- Disclosed: 2021-05-19
- Write-up quality: 6/10
- Original: https://blog.doyensec.com/2021/05/20/graphql-csrf.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2021-05-20-graphql-csrf-html
- Tags: area:web domain:api impact:ato impact:data-leak kind:research platform:research sev:medium tech:graphql technique:recon vuln:csrf vuln:xs-leak

**Target:** GraphQL HTTP endpoint (/graphql, GraphiQL console)
**Root cause:** GraphQL servers and middleware accept application/x-www-form-urlencoded and GET requests for queries/mutations, and developers assume JSON-only content-type protects against CSRF, so no CSRF tokens or origin checks are enforced.
**Steps:**
1. Send a valid GraphQL query as `content-type: application/json` POST to `/graphql`.
2. Resend the same query body as `Content-Type: application/x-www-form-urlencoded` with `query=%7B...%7D`; server accepts it via middleware translation.
3. Generate a CSRF PoC form posting `query` field to the endpoint (Burp Engagement Tools > Generate CSRF PoC).
4. For GET-based: load `https://victim/graphql?query=mutation...&variables=...` in an iframe to trigger mutations like `AddFile` or `SetUserEmail`.
5. For XS-Search: issue GET `?query=query{isEmailAvailable(email:"foo@bar.com"){is_email_available}}` and compare response times to binary-search data.
**How found:** Observed that middleware translates urlencoded/GET requests into JSON GraphQL, contradicting the JSON-only CSRF assumption; enumerated query names with a custom tool to find state-changing operations exposed as queries.
**Exploitation:** Attacker hosts a page with an auto-submitting form or iframe pointing at the victim's GraphQL endpoint; the victim's browser attaches session cookies, executing arbitrary queries/mutations (file upload, email change) or timing-based data enumeration without the attacker seeing responses.
**Escalation:** State-changing queries misplaced under GET plus guessable IDs allow account email takeover; timing side-channels turn CSRF into XS-Search data exfiltration.
**Insight:** Test whether a JSON-only GraphQL endpoint also parses `application/x-www-form-urlencoded` and GET `?query=`; enumerate query names (not just mutations) for state-changing resolvers, since servers often only block mutations over GET.

## Proof of concept (copied exactly from the original report)

**CSRF PoC form posting urlencoded GraphQL query**

```
<html>
 <!-- CSRF PoC - generated by Burp Suite Professional -->
 <body>
 <script>history.pushState('', '', '/')</script>
 <form action="https://redacted/graphql" method="POST">
 <input type="hidden" name="query" value="&#123;&#10;&#32;&#32;user&#32;&#123;&#10;&#32;&#32;&#32;&#32;firstName&#10;&#32;&#32;&#32;&#32;&#95;&#95;typename&#10;&#32;&#32;&#125;&#10;&#125;&#10;" />
 <input type="submit" value="Submit request" />
 </form>
 </body>
</html>
```

