<< All versions

Skill v1.0.0

currentLLM-judged scan95/100
nguyencongnamit/skills-library/graphql-security
──Details
PublishedSeptember 27, 2026 at 06:06 AM
Content Hashsha256:d41d7bb4e1b37a16...
Git SHA617c977343ba
──Files
Files (1 file, 6.8 KB)
SKILL.md6.8 KBactive
SKILL.md · 139 lines · 6.8 KB

id: graphql-security version: "1.0.0" title: "GraphQL Security" description: "Defend GraphQL APIs: depth/complexity limits, introspection in production, batching/aliasing abuse, field-level authorization, persisted queries" category: prevention severity: high applies_to:

  • "when generating GraphQL schemas, resolvers, or server config"
  • "when wiring authentication/authorization to a GraphQL endpoint"
  • "when adding a public GraphQL API gateway"
  • "when reviewing /graphql endpoint exposure"

languages: ["javascript", "typescript", "python", "go", "java", "kotlin", "csharp", "ruby"] token_budget: minimal: 1200 compact: 1500 full: 2200 rules_path: "rules/" related_skills: ["api-security", "auth-security", "logging-security"] last_updated: "2026-06-20" sources:

  • "OWASP GraphQL Cheat Sheet"
  • "CWE-400: Uncontrolled Resource Consumption"
  • "Apollo GraphQL Production Checklist"
  • "graphql-armor (Escape technologies)"

GraphQL Security

Rules (for AI agents)

ALWAYS

  • Enforce a maximum query depth (typical: 7–10) and **query

complexity** (cost) at the server. A 5-level nested query against a many-to-many relationship can return billions of nodes; without a cost limit, one client crashes the database.

  • Disable introspection in production. Introspection makes

reconnaissance trivial; legitimate clients have the schema baked in via codegen or a .graphql artifact.

  • Use persisted queries (allowlisted operation hashes) for any

high-traffic / public API. Anonymous arbitrary GraphQL is the GraphQL equivalent of eval(req.body).

  • Apply field-level authorization in resolvers, not just at the

endpoint. GraphQL aggregates many fields into one HTTP response — a single missing @auth on a sensitive field leaks data across the whole query.

  • Limit the number of aliases per request (typical: 15) and the

number of operations per batch (typical: 5). Apollo / Relay both allow batched queries — without limits this is an N-pages-of-the-API amplification primitive.

  • Reject circular fragment definitions early (most servers do, but

custom executors don't). A self-referencing fragment causes exponential parse-time cost.

  • Return generic errors to clients (INTERNAL_SERVER_ERROR,

UNAUTHORIZED) and route stack traces / SQL snippets to server logs only. Default Apollo errors leak schema and query internals.

  • Set a request size limit (typical: 100 KiB) and a request timeout

(typical: 10 s) on the HTTP layer in front of the GraphQL server. A 1 MiB GraphQL query has no legitimate use.

NEVER

  • Expose /graphql introspection on a production endpoint. The

GraphQL playground (GraphiQL, Apollo Sandbox) must also be disabled in production builds.

  • Trust the depth / complexity of a query because "our clients only

send well-formed queries." Any attacker can hand-craft a request to /graphql.

  • Allow @skip(if: ...) / @include(if: ...) directives to gate

authorization checks. Directives run after authorization in most executors, but custom directive ordering has produced authz bypasses.

  • Implement N+1 patterns in resolvers (one DB query per parent record).

Use a DataLoader or join-based fetch. N+1 is both a performance bug and a DoS amplifier.

  • Allow file uploads via GraphQL multipart (apollo-upload-server,

graphql-upload) without size limits, MIME validation, and out-of-band virus scan. The 2020 CVE-2020-7754 (graphql-upload) showed how a malformed multipart can crash the server.

  • Cache GraphQL responses by URL alone. POST /graphql always uses the

same URL; cache must key on operation hash + variables + auth claims to avoid cross-tenant leaks.

  • Expose mutations that take untrusted JSON input: objects without

schema validation. GraphQL types are mandatory at the schema layer, but JSON / Scalar types bypass them entirely.

KNOWN FALSE POSITIVES

  • Internal admin GraphQL endpoints behind an authenticated VPN may

legitimately leave introspection on for developer ergonomics.

  • Static-allowlisted persisted queries make depth / complexity checks

redundant on those operations — keep the checks for any operation that isn't in the allowlist (i.e. operations through a disabled flag).

  • Public, read-only data APIs may use very high cost limits with

caching aggressively configured at the CDN layer; the trade-off is documented per endpoint.

Context (for humans)

GraphQL gives clients a query language. That language is Turing-complete in practice — depth, aliasing, fragments, and unions combine to form near-arbitrary computation against the resolver graph. Treating /graphql as a single endpoint with simple WAF / rate-limit controls is inadequate.

The 2022-2024 era of GraphQL incidents (Hyatt, Slack research from Apollo, several account-takeover-via-batching cases) all hinged on either missing field-level authorization or missing cost analysis. graphql-armor (Escape) and Apollo's built-in validation rules now provide off-the-shelf middleware for most of these — use them.

Verify & lock (triaging a finding)

A scanner/review hit is a candidate, not a confirmed bug. Confirm it, fix it, then lock it so it can't come back.

  1. Confirm it's real (probe the suspect input). POST directly to /graphql

(bypass your client — attackers do). For introspection: send { __schema { types { name } } }; a real hit returns the full type list (and GraphiQL/Sandbox loads) — an FP returns a generic error or 400. For DoS: send a deeply-nested recursive query (e.g. user { friends { friends { friends { … } } } } past your depth limit) or one with 50+ aliases / a batch of many operations; real if it hangs, spikes DB load, or amplifies — FP if rejected with a depth/complexity/alias error. For authz: request a sensitive field as a low-priv user; real if it returns data instead of UNAUTHORIZED.

  1. Fix, then lock with a regression test (unit or integration — dev's call):

assert introspection is OFF in the prod config ({ __schema } → error, not schema); assert a query exceeding the depth/complexity/alias limit is rejected before execution; assert a sensitive field returns UNAUTHORIZED for an unauthorized caller. Include a benign case that must still pass — a normal shallow query under the limits returns data, and an authorized caller reads the field. Commit it to CI so the guard can't be silently dropped in a later refactor.

References

All versions