Safeguard
Vulnerability Analysis

GraphQL injection and introspection abuse explained

How GraphQL injection, introspection abuse, and alias-based DoS attacks expose APIs to data leaks—illustrated by the 2021 Peloton breach.

Bob
Application Security Engineer
7 min read

GraphQL now sits behind the APIs of GitHub, Shopify, Facebook, and thousands of internal microservices, but its flexible query model opens two attack surfaces REST APIs mostly avoid: injection through unsanitized resolver arguments, and introspection abuse that hands attackers a complete map of your schema for free. In January 2021, researchers at Pen Test Partners showed how an unauthenticated GraphQL endpoint at Peloton let anyone query private user data — age, gender, weight, and workout history — for any of the company's roughly 3 million connected fitness members. The same design that makes GraphQL efficient for frontend teams, fetching exactly the fields a screen needs in one round trip, also lets attackers batch hundreds of queries into a single HTTP call, walk deeply nested relationships until a server exhausts memory, or simply ask the API to describe itself field by field. This post breaks down how those techniques work, with concrete examples, and what to check in your own schema.

What is GraphQL injection, and how does it differ from SQL injection?

GraphQL injection happens when a resolver passes a user-supplied query argument directly into a downstream database call, template, or system command without validation — the same root cause as SQL injection, but triggered through a resolver's implementation rather than a flaw in the query language itself. GraphQL has no equivalent of a '; DROP TABLE syntax bug at the protocol level; the risk lives entirely in what each resolver does with its arguments. A common pattern: a search(filter: String) argument gets concatenated into a MongoDB query, enabling NoSQL operator injection through payloads like {"$ne": null} or {"$regex": ".*"} to bypass filters. Another: a updateProfile(bio: String) mutation builds a raw SQL UPDATE string instead of using parameterized queries, producing classic UNION-based SQL injection identical to a REST equivalent — except GraphiQL's autocomplete and type hints make it trivial for an attacker to discover the exact mutation signature and argument types needed to craft the payload, without ever reading source code. Server-side request forgery follows the same shape when a resolver accepts a URL argument — for instance an importFromUrl(source: String) field on a media-upload mutation — and fetches it without validating against an internal-IP allowlist, letting an attacker point the server at 169.254.169.254 to reach cloud metadata endpoints. In every case, the fix is identical to REST: parameterize queries, validate and allowlist inputs, and never trust a resolver argument just because it passed GraphQL's type system, since type-checking confirms an argument is a String, not that it's safe.

What is introspection abuse, and why is it enabled by default in most GraphQL servers?

Introspection abuse is the use of GraphQL's built-in __schema and __type meta-queries to enumerate every type, field, argument, and mutation an API exposes, effectively handing an attacker a full API specification with zero prior knowledge of the backend. It's enabled by default because it powers legitimate developer tooling — GraphiQL, Apollo Studio's schema explorer, and codegen tools all depend on it — and most reference implementations shipped it on out of the box. Apollo Server didn't disable introspection in production by default until version 3, released in September 2021; frameworks including graphql-yoga and express-graphql still ship with it enabled unless a developer explicitly turns it off. Tools built specifically to exploit this are mainstream: the InQL Burp Suite extension (Doyensec, 2019) auto-generates a full query library from a live introspection response, and clairvoyance reconstructs a usable schema through field-name brute-forcing and error-message analysis even when introspection has been disabled outright.

How do attackers use query batching and aliasing to bypass rate limits?

Attackers use GraphQL's alias syntax to pack dozens or hundreds of logically distinct operations into a single HTTP request, so a rate limiter counting requests-per-second sees exactly one call while the resolver layer executes hundreds of operations underneath it. A concrete example, documented in PortSwigger's Web Security Academy labs: an attacker aliases a login mutation 100 times in one POST body — attempt1: login(password: "password1"), attempt2: login(password: "password2"), and so on — defeating a limiter configured for 10 requests per minute per IP, because the limiter has no visibility into how many operations live inside that one request. Most server implementations impose no default cap on alias count or batch size; graphql-java, for instance, requires a developer to explicitly wire in MaxQueryComplexityInstrumentation or an alias-count validation rule, since neither ships enabled by default.

What is a GraphQL denial-of-service attack via query depth or complexity?

A GraphQL denial-of-service attack sends a single deeply nested or fragment-heavy query that forces the server to resolve an exponentially growing number of database calls from one HTTP request, often exhausting CPU or memory before any request timeout fires. A typical payload nests a relationship 15–20 levels deep — friends { friends { friends { friends { ... } } } } } — turning one query into thousands of downstream lookups. GitHub's public GraphQL API caps queries at 500,000 total "points" of node cost specifically to blunt this class of attack, a limit documented directly in GitHub's own API reference. A related variant, fragment bombing, spreads a handful of fragments into each other recursively — 10 self-referencing fragments can expand a query that looks like a few hundred bytes on the wire into millions of field selections at execution time. OWASP formalized this risk category as API4:2023, Unrestricted Resource Consumption, in the 2023 API Security Top 10. Mitigations are well established but rarely applied by default: graphql-cost-analysis and Apollo's graphql-query-complexity library let a team assign a numeric cost to each field and reject any query above a threshold before execution even begins, and pairing a hard depth limit — commonly 6 to 10 levels — with a per-field cost weight stops both the nesting attack and the fragment-expansion variant with one control.

What happened in the Peloton GraphQL exposure, and what does it reveal about API authorization?

The Peloton exposure happened because an unauthenticated GraphQL endpoint let any caller query another user's profile fields — age, gender, weight, city, and workout metrics — even when that user had marked their profile private, a flaw Pen Test Partners privately reported in January 2021. Peloton fixed part of the issue quickly but left the unauthenticated data-access path open until roughly May 2021, when it was patched only after press inquiries. The underlying bug wasn't in GraphQL as a technology; it was broken object-level authorization (BOLA), OWASP's API1:2023 category — the API checked that a caller was authenticated but never checked whether that caller was authorized to view a specific other user's fields. GraphQL's single-endpoint design makes this easier to miss during review than in REST, because there's no per-route middleware layer to audit: every field-level authorization decision has to be implemented correctly inside the resolver itself, field by field, with no framework-level backstop.

How Safeguard Helps

Safeguard's Griffin AI models GraphQL resolvers as part of your application's call graph, so when a scan flags a known vulnerability in graphql-java, express-graphql, or Apollo Server, reachability analysis confirms whether an untrusted query argument actually flows into the affected code path — not just whether the package shows up in your SBOM. Safeguard generates and ingests SBOMs across your GraphQL services continuously, tracking which schemas still expose introspection in production and which resolvers are missing query-complexity limits, depth limits, or per-field authorization checks. When Griffin AI confirms a reachable issue — an uncapped alias count on a login mutation, or introspection left on for an internet-facing endpoint — Safeguard opens an auto-fix PR with the specific remediation, such as a complexity-cost directive, a schema-introspection rule, or persisted-query allowlisting, instead of a generic advisory. That turns a GraphQL security review that used to take an engineer a full day of manual GraphiQL probing into a prioritized, reachability-verified backlog a team can clear in a single sprint.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.