Tickets Data Exfiltration/Modification Via CSRF After Bypassing Restrictions on a GraphQL API Endpoint
Intro
In a well-known website that provide IT support functionalities, like zendesk. It was using a GraphQL API endpoint in some features like ticketing to do operations or retrieve queries.
One of these features that uses that endpoint was the ticketing feature.
Ticketing feature — if you are not familiar with — like that used in support websites, a user provides his problem/issue, then one of the support team replies to or escalates the issue to another support user. (like used in HackerOne, when submitting a vulnerability, a ticket is created, then HackerOne’s Triage/internal team replies to triage the report)
After using the feature, I have noticed when a user
- creates a ticket, the
CreateTicketmutation triggered. - updates a ticket, the
UpdateTicketmutation triggered. - deletes a ticket, the
DeleteTicketsmutation triggered.
When updating a ticket, I have noticed that the user can add a participant while adding a comment. When he does that, the previous ticket comments and ticket description are sent to the user’s email. If I could do a successful CSRF attack to force the victim to update his ticket by adding my email (attacker-email) as a participant, I could exfiltrate the victim’s ticket data (his comments and description).
Testing CSRF on GraphQL endpoint
Bypassing sameSite Lax
First, I checked what sameSite cookie attribute was set, and the result was lax. Based on that, to do a successful CSRF attack, we need to make the request via the GET method.
I started by checking whether the GraphQL endpoint accepts the GET method or not. I observed a simple GraphQL query in the Burp history, then converted it to GET format.
The standard GraphQL GET format:
query=<GraphQL query>operationName=<operation name>variables=<JSON object>(URL-encoded)
So, by placing the mutation query in the “query” parameter, the GraphQL variables body in the “variables” parameter, and the operationName in the “operationName” parameter. My request was:
GET /graphql?query=query%20GetTags%20%7B%0A%20%20tags%20%7B%0A%20%20%20%20name%0A%20%20%20%20__typename%0A%20%20%7D%0A%7D%0A&operationName=GetTags&variables=%7B%7DI issued the request via Burp Repeater, and the server responded with a response body like the one returned when using the POST method. This confirms we can bypass the Lax protection via the GET method.
NOTE: If you didn’t notice a simple query like that, you can test with the following introspection query (default in GraphQL). If introspection is disabled, you can ask AI to convert any GraphQL request you have noticed (or the request you’re testing) to a GET method, then issue it. Introspection query:
GET /graphql?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7DUnderstanding the request body parameters used when issuing the updateTicket mutation request
Before converting the full request body of the updateTicket mutation to GET, I tried to send it successfully using POST to make sure there weren't other protections against CSRF, like random values (csrf-token).
Bypassing non-predictable values
To do a successful CSRF attack, we should provide correct values in these parameters. However, on the victim’s behalf, I don’t have his uid, requesterUid, version values, and other parameters' values in the request.
issuerUid/issueUidThere were mandatory parameters (victim “issuerUid”/”issueUid”) included in the request but I can’t guess/extract. I checked their values, they were hex format containing 32 digits, but the tricky part here is the app checks whether the value is in hex format or not. So, I bypassed this by setting a valid hex-format value.version. This was a counter that must be updated when the ticket is updated twice. For example: when the victim updates the ticket the first 2 times, “version”:1, the 3rd/4th requests must be “version”:2, and so on. I bypassed this by brute-forcing this field on the victim’s behalf, starting with 1 (via JavaScript). *You can use GraphQL aliases to bypass this — it worked for me on another GraphQL endpoint, but not on this one.requesterUid. There were other fields but not mandatory. If you provided the field, you had to provide its correct value, but if you didn’t provide the field, there was no error about a required value. I bypassed this by identifying the non-mandatory fields that I could omit without triggering errors. Removing them returned OK.Uid. There were others mandatory fields but its value not required. one of them was “Uid”. Case: “Uid”:”hexValue_notCorrect” ==> error. case: “Uid”: “” ==> ok
After issuing the request successfully via Burp Repeater without needing any values that I don’t have, this confirms we can launch the attack successfully.
Finally, I converted the POST method to GET and the POST request body to GET request format (as I mentioned above).
I created a CSRF PoC that does the attack, then delivered it to the victim. When the victim clicked on the link, their ticketing data was exfiltrated to the attacker’s SMTP server.
Note, here, the ticket comments and description were sent to the attacker’s server. Additionally, the attacker added a comment (“comment by the attacker” in the pic). So, the attacker can force the victim to update his ticket, exfiltrating his ticket data to the attacker’s SMTP server.
Escalating the impact — Exfiltrating all victim tickets
I asked myself why I didn’t try exfiltrating all of the victim’s ticket data (not just one ticket). There is no rate-limit protection, and the ticket IDs start with 1000. First ticket created is 1000, second ticket is 1001, third is 1002, and so on. So, I updated the CSRF PoC to brute-force ticket IDs and trigger the attack on all tickets on the victim’s behalf.
Impact
- An attacker can exfiltrate all tickets’ data (description and comments) across the entire platform, not just a single victim — since ticket IDs are sequential and there’s no rate limiting.
- Because the attack adds the attacker as a participant, it’s not a one-time leak — the attacker keeps receiving future updates on the victim’s ticket until removed, turning this into persistent, ongoing exfiltration rather than a single snapshot.
- An attacker can also modify ticket data (add comments, change fields) on behalf of the victim, affecting data integrity — not just confidentiality.
- The attack requires only one click from the victim (CSRF via a crafted link), with no further interaction needed, making it low-complexity to execute at scale.
HackerOne PoC
It was submitted before by another researcher, but with a lower impact. The vulnerable endpoint was /graphql endpoint. The researcher submitted the report contains a graphql mutation with lower impact (doesn’t require many restrictions bypasses, it was a direct exploit, there are any non-predictable values like Uid, requesterUid,..), while I exploited another graphql mutation leads to a higher impact but unfortunately closed as a duplicate because the two mutations work on the same endpoint.
I hope this article helped you. Happy hacking :)
My linkedIn profile: https://www.linkedin.com/in/mohamed-m-mourad/