PS Product SecurityKnowledge Base

๐Ÿ”Œ 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

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.

Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.