~/home/study/graphql-field-level-authorization

GraphQL Field-Level Authorization Bypass - A Hands-On Guide

Learn how missing or mis-configured field-level access controls let attackers read or modify data they shouldn't. The guide covers reconnaissance, crafting malicious queries, and validating bypasses with common tools.

Introduction

GraphQL has become the de-facto API layer for modern web and mobile applications. While its flexible query language is a developer boon, it also introduces a subtle attack surface: field-level authorization. Unlike REST where each endpoint can be protected individually, a single GraphQL endpoint often resolves many fields, each needing its own access check. When those checks are missing or overly permissive, an attacker can retrieve or mutate data that should be hidden.

Understanding and exploiting these gaps is critical for penetration testers, bug bounty hunters, and security engineers tasked with hardening APIs. Real-world incidents-such as the 2023 breach of a SaaS CRM that exposed customer PII through an unchecked email field-demonstrate the tangible risk.

Prerequisites

  • Solid grasp of GraphQL schema definition language (SDL) and query syntax.
  • Familiarity with GraphQL introspection queries for enumerating types, fields, and directives.
  • Basic knowledge of HTTP tools (curl, Burp Suite) and JavaScript/Node.js for crafting resolvers.

Core Concepts

At the heart of the problem is the distinction between schema-level visibility and resolver-level enforcement:

  1. Schema-level visibility: The GraphQL schema describes what fields exist. By default, all fields are discoverable via introspection unless the server disables it.
  2. Resolver-level enforcement: Each field has a resolver function that fetches data. Authorization must be performed inside that resolver (or via middleware) because the schema itself cannot express “only admin may see this”.

Common misconfigurations include:

  • Disabling introspection but still exposing sensitive fields.
  • Using a global isAuthenticated guard but forgetting per-field checks for role-based access.
  • Relying on GraphQL directives (@auth) without proper server-side implementation.

When any of these are absent, an attacker can craft a query that pulls data from a protected field.

Understanding GraphQL field-level ACLs and common misconfigurations

Most GraphQL frameworks (Apollo Server, GraphQL-Java, graphql-go) let you attach middleware or wrap resolvers. A typical ACL pattern looks like:

const isAdmin = (resolver) => async (parent, args, ctx, info) => { if (!ctx.user || ctx.user.role !== 'admin') { throw new Error('Unauthorized'); } return resolver(parent, args, ctx, info);
};

const resolvers = { Query: { secretData: isAdmin(async () => { return await db.secret.find(); }) }
};

Misconfigurations often arise when developers:

  • Wrap only top-level queries but forget nested object fields (e.g., User.email inside users list).
  • Apply the guard conditionally based on request path, inadvertently allowing bypass via aliasing.
  • Use a permissive default resolver (defaultFieldResolver) that returns raw DB rows without checks.

These gaps are exploitable because the GraphQL engine will happily resolve any field the client asks for, as long as the resolver does not reject it.

Identifying missing or overly permissive resolver checks

Before you can exploit, you need to confirm the absence of checks. Two main reconnaissance steps:

1. Introspection enumeration

Even if the API disables introspection in production, many servers expose it on a separate endpoint or allow it when an Authorization header is present. Example introspection query:

curl -s -X POST https://api.example.com/graphql -H "Content-Type: application/json" -d '{"query":"{ __schema { types { name fields { name } } } }"}'

The response lists every type and field. Look for fields that sound sensitive (e.g., salary, ssn, creditCard).

2. Response-based probing

Send a benign query requesting a suspect field and observe the error or data returned. If the field resolves to null without an error, it may be silently filtered-still a sign of a guard that could be bypassed with crafted arguments.

curl -s -X POST https://api.example.com/graphql -H "Content-Type: application/json" -d '{"query":"{ user(id: \"1\") { id email } }"}'

If email returns a value for a non-admin user, you have found a missing check.

Crafting queries to access unauthorized fields or objects

Once you know a field exists, you can leverage GraphQL's flexibility to force the resolver down a path that skips the guard. Common techniques:

  • Alias abuse: Some naive guards compare info.fieldName against a whitelist. By aliasing a protected field to an allowed name, you can slip through.
  • Fragment injection: Place the protected field inside a fragment that is conditionally included based on a variable you control.
  • Nested resolver chaining: If a top-level resolver checks the user role but returns a raw object, downstream field resolvers may not repeat the check.

Example alias bypass:

query { user(id: "1") { id publicEmail: email # Alias "publicEmail" may be whitelisted }
}

If the server only validates publicEmail against a safe list, the original email field is still resolved and returned.

Using arguments and variables to manipulate resolver logic

Many resolvers accept arguments that affect the underlying query. By tweaking those arguments, you can force the resolver to return data outside the intended scope.

Consider a resolver that limits results by ownerId extracted from the JWT. If the resolver trusts a client-supplied ownerId argument, you can override it:

query GetOrders($ownerId: ID!) { orders(ownerId: $ownerId) { id amount customer { ssn # Sensitive field } }
}

# Variables payload
{ "ownerId": "*" }

Passing a wildcard or a different ID may cause the resolver to ignore the JWT-derived restriction, exposing other users' data.

Validating bypass success with GraphQL Playground or curl

After crafting the query, you need to confirm the server returned data you shouldn't have. Two convenient methods:

GraphQL Playground

Paste the query, hit Ctrl+Enter, and examine the JSON response. Look for fields that were previously null or missing.

curl with pretty-print

curl -s -X POST https://api.example.com/graphql -H "Content-Type: application/json" -H "Authorization: Bearer $USER_JWT" -d '{"query":"query{user(id:\"1\"){id email}}"}' | jq .

If email now contains a value, the bypass is successful.

Practical Examples

Below are two end-to-end scenarios that combine the techniques above.

Example 1 - Bypassing a Role Guard via Alias

  1. Discover the adminSecret field via introspection.
  2. Observe that the server returns null for non-admin queries.
  3. Craft an alias query:
query { viewer { id safeSecret: adminSecret # Alias to "safeSecret" }
}

Send with a regular JWT. If the response contains safeSecret with the secret value, the guard only checked the field name, not the underlying data.

Example 2 - Fragment Injection to Leak Customer PII

fragment PII on Customer { ssn creditCard { number cvv }
}

query GetCustomer($includePII: Boolean!) { customer(id: "42") { id name ... on Customer @include(if: $includePII) { ...PII } }
}

# Variables
{ "includePII": true }

If the server only validates includePII for admin users but does not re-check inside the fragment, the attacker receives full PII.

Tools & Commands

  • GraphQL Playground / GraphiQL - Interactive UI for testing queries.
  • curl - Quick HTTP client for scripted testing.
    curl -s -X POST $ENDPOINT -H "Content-Type: application/json" -d '{"query":"{ __type(name:\"User\") { fields { name } } }"}'
    
  • graphql-introspection-scanner (npm) - Automates enumeration of fields.
  • Burp Suite GraphQL Extension - Adds passive scanning for missing auth checks.
  • InQL - A Go-based tool that enumerates schema and highlights potential auth gaps.

Defense & Mitigation

Preventing field-level bypasses requires a layered approach:

  1. Enforce authorization at the resolver level for every field, not just top-level queries. Use a shared middleware that checks ctx.user and the field name.
  2. Adopt schema-directives with server-side enforcement, e.g., @auth(requires: ADMIN) implemented via a schema transformer.
  3. Disable introspection in production or restrict it to authenticated users to reduce discovery of hidden fields.
  4. Whitelist allowed fields per role and reject any request that asks for a disallowed field, regardless of aliasing.
  5. Unit-test resolvers with role-based test cases to ensure no path leaks data.
  6. Static analysis - Tools like ESLint plugins for Apollo can flag resolvers missing auth checks.

Finally, log every field access attempt and monitor for anomalous patterns (e.g., a user repeatedly querying adminSecret).

Common Mistakes

  • Relying on client-side checks - Anything sent to the client can be tampered with.
  • Protecting only top-level queries - Nested fields inherit no protection unless explicitly coded.
  • Using info.path for auth - Aliases and fragments can change the path, bypassing naive checks.
  • Disabling introspection and assuming safety - Attackers can still brute-force field names or use error-based discovery.

Real-World Impact

Field-level bypasses have led to data breaches exposing PII, financial records, and proprietary code. In 2022, a fintech startup leaked over 200k customer credit-card numbers because the cardNumber field on Transaction lacked a resolver guard. The breach cost the company $3 M in fines and remediation.

My experience in red-team engagements shows that once an attacker discovers a single unprotected field, they can often stitch together a full data model, effectively turning a “read-only” GraphQL API into an open data dump.

Trend-wise, as more organizations adopt GraphQL, the focus is shifting from transport-layer security (TLS) to internal data-flow security. Investing in automated ACL validation during CI/CD pipelines is becoming a best practice.

Practice Exercises

  1. Introspection Hunt: Use curl to enumerate the schema of a public GraphQL endpoint. Identify at least three fields that appear sensitive.
  2. Alias Bypass: Write a query that aliases a protected field to a benign name and observe the response. Document whether the server validates the original field name.
  3. Fragment Injection: Craft a query with an @include directive controlled by a variable. Attempt to retrieve a hidden field by toggling the variable.
  4. Defensive Refactor: Take a sample resolver (provided below) and add a reusable authGuard that enforces role checks for every field.
    const resolvers = { User: { email: (parent, args, ctx) => parent.email, salary: (parent, args, ctx) => parent.salary, },
    };
    

Further Reading

  • “Securing GraphQL APIs” - OWASP GraphQL Cheat Sheet.
  • “Authorization in Apollo Server” - Official Apollo documentation.
  • “GraphQL Bypass Techniques” - Blog post by PortSwigger.
  • “Principles of Least Privilege in API Design” - IEEE Security & Privacy.

Summary

  • Field-level ACLs must be enforced inside every resolver; schema alone provides no security.
  • Introspection, aliasing, fragment injection, and argument manipulation are primary vectors for bypass.
  • Validate exploits with tools like GraphQL Playground or curl, and always verify server responses.
  • Mitigate by applying consistent middleware, disabling introspection, and testing resolvers for missing checks.

By mastering these concepts, security professionals can both uncover hidden data leaks and harden GraphQL services against future attacks.