๐ GraphQL and gRPC Security Review Patterns โ Complexity, Introspection, Resolver Auth, Streaming, and Abuse Resistance
Intro: GraphQL and gRPC compress a lot of power into fewer, richer interfaces. That improves developer experience, but it also changes how discovery, authorization, and resource-abuse review must work.
What this page includes
- GraphQL-specific controls for complexity, introspection, and resolver authorization
- gRPC-specific controls for mTLS, metadata auth, deadlines, streaming, and reflection exposure
- example configs and reviewer prompts
Why these protocols need dedicated review
Neither GraphQL nor gRPC fails in exactly the same way as path-based REST.
- GraphQL tends to fail through schema discoverability, nested query cost, batching, and resolver-level authorization gaps.
- gRPC tends to fail through service overexposure, weak transport/auth configuration, missing deadlines/cancellation, and unsafe streaming assumptions.
Part 1 โ GraphQL
Core security concerns
| Topic | What to review |
|---|---|
| Introspection | is production schema discoverability restricted where appropriate? |
| Depth limits | can nested queries recurse deeply enough to hurt availability? |
| Complexity / cost limits | can one request trigger disproportionate backend work? |
| Resolver authorization | are authz checks enforced in nested resolvers and object fetches, not just top-level operations? |
| Pagination | are list-returning fields bounded and predictable? |
| Persisted queries | can you reduce arbitrary query submission in production? |
| Error handling | do validation / resolver errors leak internals? |
| Batched requests / alias abuse | can one HTTP request trigger many logical expensive operations? |
Review rule #1 โ treat resolver auth as the real auth surface
Path-based API review is not enough. In GraphQL, a safe top-level mutation can still leak data if nested fields, object loaders, or child resolvers skip object-level or tenant-level authorization.
Review rule #2 โ depth limits alone are not enough
A shallow query can still be expensive if it requests many lists, aliases, or fields backed by slow joins, fan-out service calls, or heavy aggregations.
Use both:
- depth limit, and
- complexity / cost limit.
Example: Apollo-style baseline
import { ApolloServer } from '@apollo/server';
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false, // for non-public production graphs
validationRules: [
depthLimit(6),
createComplexityLimitRule(1000)
]
});
Production control table for GraphQL
| Control | Good default |
|---|---|
| Introspection | disable for non-public production graphs; document exceptions |
| Persisted queries | prefer allow-listed / persisted operations for high-value clients |
| Pagination | mandatory on list fields above trivial scope |
| Resolver auth | enforce at field/object level where data sensitivity requires it |
| Complexity budgets | set limits based on backend cost, not aesthetics |
| Rate limiting | actor-aware and operation-aware |
| Logging | operation name, caller, depth, cost, denials, latency, error family |
Part 2 โ gRPC
Core security concerns
| Topic | What to review |
|---|---|
| TLS / mTLS | are channels encrypted and, where needed, mutually authenticated? |
| Auth metadata | how are identities or bearer credentials attached and validated? |
| Method-level authz | is access enforced per RPC method and resource? |
| Reflection | is server reflection enabled in production without a reason? |
| Streaming security | are long-lived streams bounded, cancellable, and authenticated for their full lifetime? |
| Deadlines | do clients set them, or can calls hang forever? |
| Cancellation propagation | do servers and downstreams stop work when the client is gone? |
| Flow control / message size | can clients overwhelm servers with large or endless streams? |
| Keepalive | are settings coordinated to avoid self-inflicted instability or abuse? |
gRPC transport baseline
- require TLS everywhere;
- use mTLS for internal service-to-service trust where workload identity matters;
- do not rely only on network location;
- keep channel auth separate from RPC authorization.
Example: Go gRPC client with TLS and deadline
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
creds := credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS13,
})
conn, err := grpc.DialContext(
ctx,
"billing.internal:443",
grpc.WithTransportCredentials(creds),
)
if err != nil {
return err
}
defer conn.Close()
Streaming security
Streaming RPCs deserve explicit review because they are long-lived and stateful.
What can go wrong
- no deadline or cancellation handling, so streams sit open indefinitely;
- server keeps processing after the client disconnects;
- backpressure assumptions are wrong and resource use grows unbounded;
- auth is checked only once, but long-lived stream semantics change over time;
- message size or event rate is not bounded;
- keepalive is set too aggressively and becomes a reliability or DoS problem.
Streaming checklist
- realistic deadlines or maximum session durations are set
- server handles cancellation and stops downstream work
- message size and rate limits exist
- flow control is not bypassed accidentally by buffering everything in memory
- authorization assumptions for the stream are documented
- reflection is restricted if not needed
gRPC reviewer prompts
- Is server reflection enabled in production? Why?
- Can a bearer token be replayed across services because metadata is forwarded too freely?
- What stops a client from opening many streams and idling them?
- Which RPC methods are internet-facing, and which are strictly internal?
- If a stream is authorized at start, what happens when tenant context or entitlements change during its lifetime?
GraphQL vs gRPC quick comparison
| Topic | GraphQL | gRPC |
|---|---|---|
| Discoverability risk | introspection / schema docs | reflection / service listing |
| Main abuse risk | deep / expensive query shapes | unbounded streams, no deadlines, message / concurrency abuse |
| Auth mistake pattern | top-level auth only, nested resolvers leak | transport auth assumed sufficient, method authz missing |
| Performance guardrails | depth, complexity, pagination, persisted queries | deadlines, flow control, message-size limits, cancellation |
| Logging focus | operation name, cost, depth, resolver failures | method, peer identity, deadline exceeded, cancellation, stream stats |
Recommended operating pattern
For GraphQL
- disable introspection in non-public production graphs unless explicitly needed;
- use persisted queries or allow-listing for high-value clients;
- enforce depth + complexity + pagination;
- test resolver authorization below the top level.
For gRPC
- require TLS and strongly prefer mTLS for internal service trust;
- set deadlines by default;
- propagate cancellation;
- review streaming RPCs as resource-management and authorization problems, not just protocol features.
Related pages
- GraphQL Security Review and Abuse Patterns
- API Authentication and Authorization
- API Abuse Resilience and Rate Limits
- Internal PKI for Microservices โ mTLS and Certificate Automation
Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.