Apollo Server audit patterns
TL;DR: Apollo Server-specific bugs cluster around:
contextbuilder leaking secrets, plugins running after auth, federated subgraph trust, Apollo Studio reporting leaking data, persisted query allowlist bypasses, cache directives leaking across users, and CSRF prevention disabled. Newer Apollo (v4+) has saner defaults; legacy v2/v3 deployments often retain risky configs.
What it is
Apollo Server is the dominant Node GraphQL implementation. Setup varies (express middleware, standalone, lambda, Cloudflare Workers). Audit knobs:
contextfunction builds per-request context (auth, db handles).pluginsrun lifecycle hooks (request received, parse, validate, execute, response).formatErrorshapes error responses.csrfPrevention,introspection,cacheoptions.
Bug patterns
1. Context builder leaks secrets / cross-tenant
1
2
3
4
5
context: async ({ req }) => ({
user: await getUser(req),
db, // shared
internalApiKey: process.env.KEY, // leaks if any resolver echoes context
})
Context fields reachable from any resolver. If a resolver mistakenly returns context (debug code, error handler), secrets exposed.
- Fix: minimum-viable context; never put secrets in fields a resolver could return.
2. Auth check in wrong layer
- Express middleware checks auth before Apollo. But Apollo runs the OPERATIONS regardless if reached. Middleware that fail-opens or only checks Cookie presence (not validity) → unauth queries.
- Apollo plugin
requestDidStartcan be auth — but runs after parse. Errors there have already revealed schema info via parse-error messages.
3. Plugin order leaks
- Logging plugin first → logs request body including secrets.
- Apollo Tracing plugin enabled in prod → returns performance data including resolver names and execution times (timing oracle).
4. formatError doesn’t redact
- Default behaviour returns the raw error object.
- Production should redact stack, message specifics, error codes that leak schema.
- Audit: production
NODE_ENVactually set;formatErrorfunction present.
5. Federated subgraph trust
- Apollo Federation: gateway routes parts of a query to multiple subgraphs.
- Trust assumption: gateway has authoritative auth; subgraphs trust gateway-supplied user context via
_entities. - Bug: subgraph also exposed publicly → attacker hits it directly, bypassing gateway auth.
- Fix: subgraphs network-isolated to gateway only; or each enforces own auth.
6. Apollo Studio / usage reporting
APOLLO_KEYenables sending operations + variables to Apollo Studio.- Variables can contain PII, secrets.
- Off by default if no key; on by default in tutorials/examples.
- Audit:
APOLLO_KEYenv presence;sendVariableValuesconfig (mask sensitive).
7. Persisted query allowlist bypass
- Persisted queries (PQ): client sends
extensions.persistedQuery.sha256Hashonly; server looks up cached query. - Common as anti-DoS. Some deployments allow arbitrary queries when hash unknown (fallback). Attacker sends arbitrary query with random hash → bypasses allowlist.
- Fix:
persistedQueries.ttl: 0or no fallback; allowlist mode strict.
8. @cacheControl directive cross-tenant
- Apollo’s
@cacheControl(maxAge: 300)integrated with response-cache plugin. - Cache key by default does NOT include user identity. Authed query result cached → next user sees it.
- Fix:
cacheKeyArgsplugin override, orscope: PRIVATEon user-specific fields.
9. Introspection in prod
introspection: truedefault in dev, false in prod for v4. Older configs may flip.- Audit: explicit
introspection: process.env.NODE_ENV !== 'production'.
10. CSRF prevention
- v4 default ON. v3 / v2 deployments often disabled or absent.
- Without it, simple POST forms can submit GraphQL queries → CSRF on mutations from any origin.
11. Subscription transport
- Apollo Subscriptions over WebSocket. Older
subscriptions-transport-wshad auth-on-connect-only model. Newgraphql-wsis current. - Audit: which lib is used; per-message auth.
12. Schema directives without resolver wiring
@auth(role:"admin")declared in SDL butschemaTransformersnot applied → directive is metadata only, no actual check.
Apollo Server v4 config audit
1
2
3
4
5
6
7
8
9
10
new ApolloServer({
schema,
introspection: false, // ✓ in prod
csrfPrevention: true, // ✓ default v4
formatError: (err) => ({ message: err.message }), // ✓ redact
plugins: [
ApolloServerPluginCacheControl({ defaultMaxAge: 0 }), // ✓ default
ApolloServerPluginUsageReporting({ sendVariableValues: { none: true } }), // ✓ no PII
],
});
Grep starter
1
2
3
4
5
6
7
8
rg -n 'new ApolloServer\(' src/
rg -n 'introspection:\s*true' src/
rg -n 'csrfPrevention' src/
rg -n 'formatError' src/
rg -n 'ApolloServerPluginUsageReporting' src/
rg -n 'context:\s*\(\s*\{' src/ # context builder
rg -n '@cacheControl' src/ # cache directive scope
rg -n 'persistedQueries' src/