GraphQL Query Whitelisting for Production Environments
AI generated
{ }
type
GraphQL · Whitelisting · Production Security · Apollo Router
GraphQL Query Whitelisting
rolling it out correctly for production

An open GraphQL endpoint lets any client formulate arbitrary queries against the schema, including ones never intended for production use. Query whitelisting flips this principle: only previously known, reviewed queries get executed, everything else is rejected before it ever reaches a resolver.

16 min read Manifest · Safelisting · Warn Mode · Introspection GraphQL · Security · Production Ops

1. Why open endpoints are a risk in production

A GraphQL schema is self-describing through introspection, which makes development convenient but simultaneously hands an attacker the complete map of every type, field, and relationship. Without query whitelisting, any client reaching the endpoint can formulate arbitrarily deeply nested, arbitrarily complex queries that go far beyond what the actual frontend would ever use. That's the core difference from classic REST APIs: a REST endpoint only exposes the operations a developer explicitly implemented, an open GraphQL endpoint exposes everything the schema theoretically allows.

In production environments with sensitive data or high traffic, that's a double risk: attackers can attempt to bypass access controls or extract information never intended for a frontend through unexpected query combinations, and a single, deliberately overcomplex query can generate enough backend load to overwhelm the service. Query whitelisting limits the attack surface to exactly the queries a team has reviewed and approved in advance, regardless of what the schema would theoretically permit.

2. How query whitelisting works: a hash instead of free text

The technical principle behind query whitelisting is simple: instead of transmitting and parsing the full query text on every request, the client sends only a short hash uniquely identifying a previously registered query. The server keeps a lookup table from hash to full query text, usually a SHA-256 digest of the query, and only executes requests whose hash exists in that table. Any request with an unknown hash, or with raw query text that doesn't match the registered version, gets rejected.

This principle differs from purely performance-oriented persisted queries in that, in whitelisting mode, the server never registers a new, unknown query, even if the client supplies the full query text. With classic Automatic Persisted Queries, the server would accept and cache a new query on its first full call. In production whitelisting mode, that exact automatism is disabled: the set of allowed queries is extended exclusively through a controlled build and deploy process, never at runtime by a client.


# This exact query text was registered in the manifest during the frontend build
query ProductDetail($sku: String!) {
  product(sku: $sku) {
    name
    price
    description
  }
}

# A request sending only the hash below is accepted if it matches the manifest entry
# POST /graphql
# { "extensions": { "persistedQuery": { "sha256Hash": "a1b2c3..." } }, "variables": { "sku": "TEST-001" } }

# Any query NOT in the manifest — even a harmless-looking variant — is rejected
query ProductDetailWithReviews($sku: String!) {
  product(sku: $sku) {
    name
    price
    reviews { rating comment }   # not part of any registered query — blocked
  }
}

3. The persisted query manifest as a build artifact

The heart of a working query whitelisting setup is the manifest, a file containing every allowed query together with its hash. This manifest is not maintained by hand, it's generated automatically during the frontend build: a tool scans the source code for all gql tags or .graphql files, extracts the contained operations, and computes the SHA-256 hash for each. The result is a JSON file deployed as a build artifact alongside the frontend and made known to the server.

What matters for a reliable whitelisting process is that the manifest gets updated in sync with the actually shipped code on every frontend deploy. A stale manifest still containing a query meanwhile removed from the frontend is a minor risk, whereas a manifest missing a newly added query immediately causes failing requests in the production frontend. That's why manifest generation belongs in the same CI step as the frontend build itself, not in a separate, manually triggered process.


#!/usr/bin/env bash
# generate-manifest.sh — extracts every GraphQL operation and hashes it for the manifest
set -euo pipefail

echo "[BUILD] Extracting persisted queries from source..."
npx @graphql-codegen/cli --config codegen.persisted.yml

MANIFEST_FILE="dist/persisted-queries.json"
QUERY_COUNT=$(jq 'length' "$MANIFEST_FILE")

echo "[BUILD] Generated manifest with $QUERY_COUNT registered operations"

# Fail the build if the manifest is suspiciously empty — likely a broken extraction step
if [[ "$QUERY_COUNT" -eq 0 ]]; then
  echo "[ERROR] Manifest is empty — aborting deploy" >&2
  exit 1
fi

# Publish alongside the frontend build so the server can be updated in the same deploy
cp "$MANIFEST_FILE" "dist/assets/persisted-queries.json"

{
  "format": "apollo-persisted-query-manifest",
  "version": 1,
  "operations": [
    {
      "id": "a1b2c3d4e5f6...",
      "name": "ProductDetail",
      "type": "query",
      "body": "query ProductDetail($sku: String!) { product(sku: $sku) { name price description } }"
    },
    {
      "id": "f6e5d4c3b2a1...",
      "name": "AddToCart",
      "type": "mutation",
      "body": "mutation AddToCart($sku: String!, $qty: Int!) { addToCart(sku: $sku, qty: $qty) { id } }"
    }
  ]
}

4. Apollo Router safelisting mode in practice

Apollo Router supports query whitelisting natively through its persisted query mechanism in what's called safelisting mode. Unlike the pure performance mode, where the router still accepts unknown queries as long as the full text is sent, safelisting mode consistently blocks any request whose hash isn't in the loaded manifest, regardless of whether the client supplies the query text. This configuration is an explicit setting that must be clearly separated from the default performance optimization.

The router loads the manifest either from a locally provided file or from a central registry such as Apollo GraphOS Uplink, which, with multiple router instances behind a load balancer, ensures all instances know the same set of allowed queries. For self-hosted setups without Apollo cloud services, the manifest can also be loaded from an S3 bucket or a simple HTTP endpoint updated on every deploy, as long as the router doesn't cause downtime when reloading the manifest.


# router.yaml — Apollo Router in safelisting mode: reject anything not in the manifest
persisted_queries:
  enabled: true
  safelist:
    enabled: true
    # Reject unknown queries even if the full query text is sent — no runtime registration
    require_id: true
  log_unknown: true

# Manifest source — local file updated on every frontend deploy
apq:
  router:
    cache:
      redis:
        urls: ["redis://cache:6379"]

5. Rollout strategy: warn mode before enforce mode

Activating query whitelisting immediately in full enforce mode, without a prior observation phase, is the most common cause of a production outage right after introducing it. Nearly every grown frontend contains queries missing from the manifest, whether from an overlooked code path, a feature-flag-gated query fragment, or an older mobile app version not yet updated to the new manifest. A responsible rollout therefore always starts with a warn mode, where unknown queries are logged but not blocked.

During this observation phase, typically one to two weeks, the team collects all logged queries missing from the manifest and decides individually for each whether it's legitimate and needs to be added, or whether it was actually unwanted and should stay absent. Only once the warn logs are empty, or limited to known, tolerated exceptions, for several days in a row does the team switch to full enforce mode. This two-stage rollout is mandatory for every query whitelisting project, no matter how thorough the initial manifest looks.

6. Disabling introspection in production

Query whitelisting alone isn't enough if introspection stays active, because introspection queries themselves might also be whitelisted, or worse, accidentally exempted from whitelisting. In most production GraphQL setups, the introspection query __schema should be fully disabled in production, regardless of whitelisting status, because otherwise it still reveals the complete schema structure to an attacker, even if they can no longer execute arbitrary business queries.

The trade-off: internal development and staging environments still need introspection for tools like GraphiQL, codegen, and schema diffing. The common solution is an environment-dependent configuration that only locks introspection in production, combined with a separate, authenticated introspection endpoint for internal CI pipelines that still need the schema for codegen purposes, without making it publicly accessible.


// apollo-server-config.js — introspection locked down by environment, not by guesswork
const { ApolloServer } = require('@apollo/server');

const isProduction = process.env.NODE_ENV === 'production';

const server = new ApolloServer({
  schema,
  // Disabled in production, kept on for staging/dev so Codegen and GraphiQL still work
  introspection: !isProduction,
  plugins: [
    {
      async requestDidStart() {
        return {
          async didResolveOperation({ request }) {
            if (isProduction && request.operationName === 'IntrospectionQuery') {
              // Defense in depth — reject even if introspection was accidentally left on
              throw new GraphQLError('Introspection is disabled in production');
            }
          },
        };
      },
    },
  ],
});

7. Emergency bypass and a process for new queries

A rigid query whitelisting system without a defined emergency process leads, in practice, to teams disabling whitelisting entirely under time pressure instead of specifically adding a single missing query. A better approach is a documented, fast way to approve a new query on short notice: a CI job that manually adds a single query, identified by its hash and text, to the active manifest, with mandatory two-person review, but without waiting for a full frontend deploy cycle.

Equally important is a process for the reverse case: a query that's in the manifest but turns out to be buggy or a security risk must be removable from the active manifest immediately, without waiting for the next regular deploy. Both processes, emergency addition and emergency removal, should be tested and documented before query whitelisting ever switches to enforce mode, because in a real emergency there's no time to try out an untested process for the first time.

8. Whitelisting in front of Magento GraphQL in practice

Magento's own GraphQL endpoint doesn't ship with native query whitelisting; Automatic Persisted Queries can be retrofitted via community extensions or a fronting Apollo Router, but real safelisting always requires an additional component in front of the actual Magento endpoint. In practice, this is usually handled by an Apollo Router or a comparable gateway that validates requests before they're ever forwarded to Magento, leaving Magento itself unchanged and reachable only by verified requests.

For headless storefronts with multiple frontend types, for example a web and a mobile app sharing the same Magento backend, a shared manifest is recommended, updated during every frontend build but extended per client type with its specific queries. That keeps the attack surface minimal without one frontend team accidentally blocking another team's queries, because both teams contribute to the same, centrally maintained manifest.

9. Whitelisting strategies compared

The table below compares the common approaches for allowing only known queries in production.

Strategy Allowing new queries Security level
Open endpoint, no whitelisting Anytime, uncontrolled Low, full attack surface
Automatic Persisted Queries (APQ) Automatic on the first full call Medium, performance-focused only
Manifest-based safelisting Only via a controlled build/deploy High, full control
Safelisting + introspection lockdown Only via a controlled build/deploy Very high, recommended standard

Pure Automatic Persisted Queries solve a performance problem, not a security problem, because they still accept new queries. For production APIs with real security requirements, manifest-based safelisting combined with an introspection lockdown is the recommended standard, because together both measures limit the attack surface to exactly what a team has knowingly approved.

Mironsoft

GraphQL production hardening and API security

Securing your GraphQL endpoint for production?

We set up query whitelisting with manifest generation, a warn-before-enforce rollout, and an introspection lockdown, including an emergency process for short-notice query approvals.

Manifest setup

Set up automated query extraction and hash generation in the CI build

Rollout support

Monitor warn mode and guide a safe switch to enforce mode

Emergency process

Build a tested bypass workflow for short-notice query additions

10. Summary

Query whitelisting limits a production GraphQL endpoint to exactly the queries a team has knowingly reviewed and approved, instead of leaving the entire schema open to arbitrary client requests. An automatically generated manifest, updated on every frontend build, forms the technical foundation, while Apollo Router with its safelisting mode handles enforcement, consistently blocking any request with an unknown hash.

A safe rollout always starts with a warn mode that surfaces missing queries without blocking them immediately, and only switches to full enforce mode after a clean observation phase. Combined with an introspection lockdown in production and a tested emergency process for short-notice query approvals, this produces a GraphQL setup that denies attackers the complete schema map while causing legitimate frontend teams no unnecessary day-to-day friction.

GraphQL Query Whitelisting for Production — Key Takeaways

Manifest generation

Automated in the CI build, in sync with every frontend deploy, never maintained by hand.

Safelisting mode

Blocks any hash not in the manifest, even when the query text is also supplied.

Warn-before-enforce

Log first, block later, to avoid production outages from overlooked queries.

Introspection lockdown

Disabled in production, kept available in staging and CI for codegen.

11. FAQ: GraphQL Query Whitelisting

1Whitelisting vs. APQ?
APQ automatically accepts new queries for performance. Whitelisting consistently rejects anything not pre-registered.
2Generating the manifest?
CI step scans source for operations, computes SHA-256 hashes, writes both into a JSON file.
3Why warn mode first?
Grown frontends almost always have missing queries in the initial manifest. Warn mode surfaces them without blocking.
4Whitelisting alone enough?
No, always combine with an introspection lockdown, otherwise the schema stays readable via __schema.
5Missing query in manifest?
Rejected in enforce mode. Emergency process with two-person review allows short-notice addition.
6Apollo Router native support?
Yes, via safelisting mode, explicitly configured separately from performance optimization.
7Multiple frontend types?
Shared, centrally maintained manifest, each team contributing its queries during its build.
8Magento GraphQL native?
No, requires a fronting component like an Apollo Router for request validation.
9Slows down development?
Not with a correct setup, since manifest generation runs automated in the build.
10Length of warn mode phase?
Typically one to two weeks, until logs are empty or limited to known exceptions for several days running.