skip to content

Security & Abuse Control

One endpoint that lets a client compose arbitrary queries is a denial-of-service surface before it is anything else. Interviewers ask because disabling introspection is not an answer.

part ofGraphQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

How do field aliases let one GraphQL document request the same field hundreds of times?

level: juniorimportance: must knowfreq 56%

answer

  1. One document, many response keys
  2. Renaming decides what groups together
  3. Grouping happens by key, not by name
  4. Width, not nesting depth
  5. Merging rule permits it, never collapses it

basics

~20 s

An alias sets the key a field's value appears under in the response, so the same field can be selected many times under different keys. Each aliased selection is executed separately, multiplying the server's work.

solid answer

~50 s

In an executable document a selected field's **response key** is its alias when it has one and its field name otherwise. Selections are grouped by response key, and each group is resolved once. Two selections under *different* aliases are different keys, so they are executed separately even when they name the identical field with identical arguments. Nothing in GraphQL caps how many aliases a selection set may carry, so a three-kilobyte document can select the most expensive field in a schema under 247 aliases and cause 247 executions. The validation rule that fields be mergeable applies only to selections sharing a response key, so it never fires here. This is amplification by *width*, which a depth limit, a body-size cap and an HTTP request rate limit all fail to see; static cost scoring and per-field occurrence counts do see it, because they count response keys.

code

graphql · 5 lines
graphql
query MemoryScan($locale: Locale!) {
  a1: match(source: "quarterly settlement notice", locale: $locale, first: 8400) { segmentId score }
  a2: match(source: "quarterly settlement notice", locale: $locale, first: 8400) { segmentId score }
  a3: match(source: "quarterly settlement notice", locale: $locale, first: 8400) { segmentId score }
}

go deeper

for a junior

Recall that an alias sets the response key, and that two different keys mean the same field runs twice. Be able to write a short document that selects one field three times and describe the response shape it produces.

for a middle

Explain the mechanics: fields are collected and grouped by response key, one execution per group, and the mergeability rule applies only within a group. Then show why byte-size and depth are the wrong things to measure.

for a senior

Demonstrate that you would count response keys rather than requests, and say which existing controls silently miss this — proxy rate limits, response caches keyed on document text, per-request logging — and what a realistic occurrence cap looks like on your hottest field.

for a principal

Own the framing that on a single-endpoint protocol every per-request budget is divided by document width, and decide where the accounting unit lives across the fleet so that limits, metrics and billing all agree on what one unit of work is.

## A field's key in the response is its alias, not its name When a server executes a document it does not build the response map from field *names*. It builds it from **response keys**. A selected field's response key is its alias when it carries one, and its field name when it does not. Everything about repetition follows from that single rule. Field collection walks a selection set and groups selections **by response key**. Every group becomes exactly one entry in the response map, and each group is resolved once — the selections inside a group have their sub-selection sets merged and the field is executed a single time. That is why `translator { id } translator { name }` costs one resolution of `translator` and produces one `translator` object carrying both keys. Give two selections different aliases and they land in *different* groups. They are two entries in the response map, so they are two executions — even when the aliases name the identical field with byte-identical arguments. Nothing merges them, because merging is defined per response key and these keys differ. ## The validation rule people expect to save them does not Documents are checked by a rule requiring that any two selections sharing a response key be *mergeable*: they must name the same field with the same arguments, and their sub-selections must themselves merge. The rule exists so a single response key can never have two conflicting meanings. It is a **consistency** rule, not a **deduplication** rule, and it never fires on distinct aliases, because distinct aliases are exactly the case the rule was written to permit. A document with 247 aliases over one field is not merely accepted — it is unambiguously valid, and a server that quietly collapsed those selections would be violating the response-shape guarantee that the client relies on. ## Why aliases exist at all This is not an oversight. Aliases are the only way a client can ask one question twice with different arguments and get both answers back distinguishably: two locales of the same lookup, this month's and last month's slice, an object fetched by two different identifiers. A response map keyed by field name could express only one of each. The feature and the abuse are the same mechanism. ## The arithmetic Consider a translation-memory graph whose `match(source:, locale:, first:)` field runs a fuzzy similarity search across a segment corpus — the single most expensive field in the schema. One caller sends a document under three kilobytes that selects `match(source: "quarterly settlement notice", locale: DE_DE, first: 8400)` under 247 aliases. The parse is trivial. The document passes every structural rule. Execution then performs 247 similarity searches and completes 2,074,800 rows of result — from one HTTP request, one JSON body, one entry in the access log. Amplification of this kind is *width*, and width is invisible to most of the controls people reach for first: - A **depth limit** counts nesting. The malicious document is two levels deep; a hand-written product document is often deeper. - A **body-size cap** counts bytes. Aliases are cheap to write — `a1:`, `a2:` — so the ratio of bytes sent to work caused is enormous. - An **HTTP rate limit**, whether at a reverse proxy, a CDN or the application edge, counts requests. This is one request. - A **normalized client cache** or a server response cache keyed by document text sees a document it has never seen before, so nothing is served from cache. ## What does see it The controls that notice are the ones that count *selections* rather than requests. Static cost scoring walks the collected fields and scores every response key separately, so 247 aliased `match` selections score 247 times the weight of one — and if a list-slice argument such as `first` is used as a multiplier, the score reflects the 8,400 too. A field-occurrence cap counts how many times a named field appears in a document. Both run before execution, on the document alone, which is what makes them affordable. Complementing them, per-request backend batching bounds how many *database* calls those resolutions turn into, but it does not bound the similarity searches themselves — deduplication only helps when the repeated selections are genuinely identical, and an attacker who varies one argument per alias defeats it while keeping the amplification. ## What to say in an interview Say that the response key is the unit of execution, that aliases mint new response keys, and that therefore the number of times a field runs is bounded by the document's *width*, which nothing in GraphQL bounds by default. Then name the counting unit you would enforce on: occurrences of a field per document, not requests per client. That answer shows you understand why a perfectly ordinary rate limit reports a quiet endpoint while the database is on fire.

  • Why does the validation rule requiring selections to be mergeable not collapse the repeated selections?
    That rule only constrains selections that share a response key: it forbids one key from meaning two different things. Distinct aliases are distinct keys, so the rule is satisfied trivially and has nothing to merge. It is a consistency rule, not a deduplication rule.
  • Does it change anything if the attacker varies one argument per alias?
    It makes the amplification harder to defend against. Identical selections can at least be deduplicated by a per-request batch cache or a response cache; varying a locale, a search string or a slice size per alias defeats that while keeping the multiplication. Cost scoring still counts each key, so a document-level score remains the control that holds.
  • Which controls miss alias amplification entirely?
    Anything counting the wrong unit: a nesting-depth limit sees two levels, a body-size cap sees a few kilobytes, and an HTTP rate limit at a reverse proxy or CDN sees one request. Counting response keys — occurrences of a field per document, or a static cost per key — is what catches it.

It is the difference between asking a librarian for one book and handing over 247 request slips that all name the same book — the slips are cheap to write, and each one still sends someone into the stacks.

saying these in an interview costs you the question

  • Aliases are cosmetic renaming with no execution cost
  • The server automatically merges identical field selections
  • A nesting-depth limit stops alias amplification
  • A per-request rate limit counts each aliased selection
  • Validation rejects a field selected many times
  • Aliases are only usable on query operations

context

open as a page

What security controls does the GraphQL specification itself define for a server?

level: juniorimportance: must knowfreq 55%

basics

~20 s

The GraphQL specification defines no security controls at all - only a type system, document syntax, validation rules, an execution algorithm and a response shape. Authentication, authorization, rate limiting, depth caps and cost limits are server policy.

open as a page

Why can a GraphQL document nest arbitrarily deep when the schema has finitely many types?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Object types can reference each other, so the type graph contains cycles. A finite schema therefore admits documents of unlimited depth: each trip round a cycle adds one selection level, and the GraphQL specification sets no depth limit.

open as a page

Why does an exception thrown in a GraphQL resolver end up in the client's error message?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A GraphQL server must produce a message for every error it returns, and the simplest string available is the thrown exception's own. Nothing in the specification redacts it, so driver text, file paths and internal hostnames travel straight to the caller.

open as a page

Why is authorizing only the entry field of a GraphQL query not enough to protect the data behind it?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A graph reaches the same object down many paths, so a check on the root field guards one entrance only. Every other schema field returning that object is another way in, and each needs its own check.

open as a page

What does an open introspection query on a GraphQL endpoint expose to any caller?

level: juniorimportance: must knowfreq 62%

basics

~10 s

The whole type system. One introspection result is enough to print the schema back out as SDL: every type, field, argument, enum value, interface and deprecation reason. It exposes shape, never data.

open as a page

How does a GraphQL cost limiter compute a document's complexity score before execution?

level: middleimportance: must knowfreq 58%

basics

~20 s

It walks the document's selection set, charges every field a configured weight, multiplies each list field's subtree by the slice its arguments request, and sums the result. The total is compared to a budget after validation, before any resolver runs.

open as a page

Where in GraphQL request handling do content-type checks, depth caps and deadlines each belong?

level: middleimportance: must knowfreq 47%

basics

~20 s

Each control attaches at the earliest phase that has the information it needs. Content-type and body-size checks run before parsing; depth, field-count and cost caps run as validation rules over the parsed document; deadlines and per-object authorization can only run during execution.

open as a page

How does requiring Content-Type application/json on a GraphQL POST block cross-site forgery?

level: middleimportance: must knowfreq 52%

basics

~20 s

A page cannot make a browser send a cross-origin body labelled application/json without the destination server first granting permission. Rejecting every other media type before parsing therefore means a forged POST is never dispatched at all.

open as a page

With GraphQL introspection disabled, what can a caller still infer about the schema?

level: middleimportance: must knowfreq 54%

basics

~20 s

Most of it, given time. Validation errors commonly name types and suggest near-miss field names, __typename still reports concrete type names, and a web client ships the documents it sends. Disabling introspection raises enumeration cost, it does not end it.

open as a page

Why is an operation allowlist not the same thing as Automatic Persisted Queries?

level: middleimportance: must knowfreq 52%

basics

~20 s

Both send a hash instead of document text. Automatic Persisted Queries register whatever document a client uploads on a miss, so any caller can add one. An allowlist is populated out of band and answers a miss with rejection.

open as a page

When a GraphQL operation deadline fires mid-execution, what happens to the in-flight resolvers?

level: middleimportance: must knowfreq 52%

basics

~10 s

Nothing, unless cancellation is propagated. A deadline is a timer, not a stop button: the server can abandon the response while resolvers, driver calls and backend statements keep running and keep holding connections.

open as a page

Your GraphQL login mutation is rate-limited per HTTP request. Why does that not stop credential stuffing?

level: seniorimportance: must knowfreq 53%

basics

~20 s

One HTTP request can carry the same login mutation hundreds of times — aliased under different response keys, and again as members of a batched array. A per-request counter sees one attempt while the server performs thousands, so count attempts where the field executes.

open as a page

Why do teams push GraphQL field authorization down into the data-loading layer, and what does it cost?

level: seniorimportance: must knowfreq 54%

basics

~20 s

Every traversal that reaches an object goes through the loader, so a check there covers paths nobody enumerated, including fields added later. It costs a batch cache that must be viewer-scoped, denial becoming absence, and policy lookups needing their own batching.

open as a page

One client opens a GraphQL WebSocket and starts hundreds of subscriptions — which limits stop it?

level: seniorimportance: must knowfreq 45%

basics

~10 s

Cap active subscriptions per connection and connections per identity, reject duplicates, bound each stream's event rate, and bound the outbound buffer so a client that stops reading is disconnected rather than buffered forever.

open as a page

Why can a shallow GraphQL document still be expensive enough to need a cost limit?

level: juniorimportance: should knowfreq 44%

basics

~20 s

Cost follows how many objects a document asks the server to produce, not how deeply it nests. Three levels of list fields, each requesting a few hundred items, multiply into millions of objects while staying shallow.

open as a page

What is a GraphQL operation allowlist, and what abuse does registering documents close?

level: juniorimportance: should knowfreq 40%

basics

~20 s

An operation allowlist registers the exact executable documents clients are allowed to send. The server accepts a request only when its document is one of those, usually referenced by an identifier, and rejects every other document before parsing it.

open as a page

What does a per-operation timeout on a GraphQL server bound that a cost limit cannot?

level: juniorimportance: should knowfreq 42%

basics

~20 s

A cost limit scores the document's shape before anything runs, so it never sees a clock. A per-operation deadline bounds wall-clock time — the slow backend call, the lock wait, the cold cache — which no static score can predict.

open as a page

Why does a GraphQL depth cap need a field-count cap and a body-size cap alongside it?

level: middleimportance: should knowfreq 44%

basics

~20 s

A depth cap bounds only the longest path. A shallow document can still select hundreds of fields at every level, and any document must be parsed in full before validation runs, so breadth and raw bytes each need their own limit.

open as a page

How do you mask unexpected GraphQL errors without going blind in production?

level: middleimportance: should knowfreq 52%

basics

~20 s

Replace an unexpected failure's message with a fixed string, attach a correlation identifier the caller can quote, and log the full exception under that same identifier. The client learns nothing internal, and an engineer can still find the exact failure.

open as a page

In a GraphQL schema, what can an authorization directive on a field decide, and what must a resolver still check?

level: middleimportance: should knowfreq 48%

basics

~20 s

A directive on a schema field carries a static rule — the role or scope the viewer must hold — which the server enforces uniformly. It cannot decide whether this viewer is party to this particular record; only a resolver holding the loaded object can.

open as a page

Your GraphQL cost limiter began rejecting a shipped mobile app's query after a schema change — how do you diagnose and fix it?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Log every operation's score, its budget and the path that contributed most, then replay the rejected document against the weight tables before and after the deploy. A rename that adds charged levels inside a list multiplier is the usual cause.

open as a page

Why can't per-object authorization be enforced as a GraphQL validation rule?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Validation runs over the document and schema before any resolver executes, so the object does not exist yet. Whether this caller may read this particular order can only be decided once the order has been loaded - during execution.

open as a page

A GraphQL server parses any POST body as JSON whatever the Content-Type says. How is that forged?

level: seniorimportance: should knowfreq 33%

basics

~20 s

A plain HTML form can send a cross-origin body as text/plain, with cookies attached and no permission step. Split across a field name and value, that body is valid JSON, so a lenient server executes it.

open as a page

You add a maximum-depth rule to a public GraphQL API and real traffic starts failing. How do you set the cap?

level: seniorimportance: should knowfreq 51%

basics

~20 s

Measure before enforcing. Log the depth of real documents through a full traffic and release cycle, then set the cap well above the legitimate maximum. Expect introspection and connection-wrapped traversals to be the deepest legitimate documents.

open as a page

How do you keep deliberate, client-actionable GraphQL errors legible while masking the rest?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Classify at the throw site, never by inspecting message text. A failure explicitly marked client-facing keeps its reviewed message and stable code; everything else is masked. Masking must be the fallback, so an unclassified failure is never legible by accident.

open as a page

A pentest report says introspection is enabled on your GraphQL API — how do you triage it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Rate it for what it enables. Introspection is a reconnaissance finding, not an access-control one: disabling it raises the cost of discovering fields and changes nothing about what executes. Fix the reachable operations it revealed first.

open as a page

An operation allowlist fixes which documents run — what abuse still arrives through variables?

level: seniorimportance: should knowfreq 47%

basics

~20 s

An allowlist constrains a request's shape, never its values. Every argument a registered document exposes as a variable stays caller-controlled — page sizes, record identifiers, filter strings — so range checks, authorization and rate limits still run on every request.

open as a page

How do you set a GraphQL cost budget and allocate points across callers?

level: principalimportance: should knowfreq 37%

basics

~20 s

Anchor a point in something measurable — one object fetched, or a millisecond of service time. Set the per-request cap above the p99 score of legitimate traffic, then budget each caller in points per window rather than requests per window.

open as a page

showing 1–30 of 37