Skip to main content

03. GraphQL & gRPC Security

As engineering teams adopt GraphQL for flexible data querying and gRPC for low-latency microservice communications, security models must adapt beyond traditional REST HTTP verb and endpoint patterns. Both protocols introduce unique architectural attack surfaces that require protocol-specific hardening.


1. GraphQL Security Architecture

Unlike REST APIs that expose discrete endpoints (/api/v1/users, /api/v1/orders), GraphQL operates through a single endpoint (typically /graphql), processing flexible query payloads against a unified Abstract Syntax Tree (AST).


A. Introspection Disclosure & Schema Hardening

GraphQL introspection enables clients to query __schema and __type meta-fields, retrieving the complete object schema, input parameters, hidden admin fields, and deprecated queries.

GraphQL Introspection Exploit Payload

# Introspection Query to map complete backend schema
query IntrospectSchema {
__schema {
queryType { name }
mutationType { name }
types {
name
kind
fields {
name
type { name kind }
args { name type { name } }
}
}
}
}

[!WARNING] While introspection is invaluable during development, leaving it enabled in production exposes internal data structures, administrative mutations, and unreleased feature flags to adversaries.

Production Introspection Disabling Code

Node.js (Apollo Server v4)
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginDisableIntrospection } from '@apollo/server/plugin/disableIntrospection';

const server = new ApolloServer({
typeDefs,
resolvers,
// Disable introspection conditionally in production
plugins: process.env.NODE_ENV === 'production'
? [ApolloServerPluginDisableIntrospection()]
: [],
});
Python (Strawberry GraphQL / FastAPI)
from strawberry.fastapi import GraphQLRouter
import strawberry

@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "Hello World"

schema = strawberry.Schema(query=Query)

# Disable introspection in production environment
graphql_app = GraphQLRouter(
schema,
introspection=process.env.get("ENV") != "production"
)
Go (gqlgen)
package main

import (
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
)

func NewGraphQLHandler(es graphql.ExecutableSchema, isProd bool) *handler.Server {
srv := handler.NewDefaultServer(es)
if isProd {
// Introspection is enabled by default in gqlgen; remove or block plugin
// Do not add srv.Use(extension.Introspection{}) in production!
} else {
srv.Use(extension.Introspection{})
}
return srv
}
Java (Spring Boot GraphQL)
# application.yml - Disable Introspection in Production
spring:
graphql:
schema:
introspection:
enabled: false # Set to false for production profile

B. Query Depth & Complexity DoS Attacks

Because GraphQL fields can be recursively nested, an attacker can submit a deeply nested query causing server CPU and memory exhaustion:

# Recursive Query Depth DoS Payload
query RecursiveDoS {
user {
friends {
friends {
friends {
friends {
friends {
name
}
}
}
}
}
}
}

Query Depth & Cost Calculation Solutions

  1. Depth Limiting: Restricts the maximum nesting level of a query (e.g., max depth = 5).
  2. Complexity Analysis: Assigns a numeric cost to each field (e.g., scalar fields = 1, collection fields = 10 multiplied by first/limit arguments).
Node.js Depth Limiting (graphql-depth-limit)
import depthLimit from 'graphql-depth-limit';
import { ApolloServer } from '@apollo/server';

const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5, { ignore: ['_id'] }) // Enforce maximum depth of 5
]
});
Go Depth & Complexity Limiting (gqlgen)
import (
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
)

func ConfigureServer(es graphql.ExecutableSchema) *handler.Server {
srv := handler.NewDefaultServer(es)

// 1. Enforce Max Query Depth
srv.Use(extension.FixedComplexityLimit(200)) // Max cost score

return srv
}
Python Complexity Guard (graphene-django / strawberry)
from strawberry.extensions import MaxTokensLimiter, MaxAliasesLimiter

schema = strawberry.Schema(
query=Query,
extensions=[
MaxTokensLimiter(max_tokens=1000),
MaxAliasesLimiter(max_aliases=10),
]
)

C. GraphQL Batching Attacks

GraphQL supports query batching via HTTP Array Batching or Alias Batching. Attackers use batching to execute hundreds of operations in a single HTTP request, effectively bypassing IP-based rate limiters and Web Application Firewalls (WAFs).

Traditional REST Auth Request:
1 HTTP Request = 1 Login Attempt (Trigger Rate Limiter after 5 attempts)

GraphQL Alias Batching Attack:
1 HTTP Request = 1,000 Login Attempts (Bypasses IP Rate Limiting!)
# Alias Batching Attack Payload - Bypassing Rate Limits
mutation BruteForceMFA {
attempt1: verifyMFA(code: "0001") { success token }
attempt2: verifyMFA(code: "0002") { success token }
attempt3: verifyMFA(code: "0003") { success token }
# ... Up to 1,000 aliases in a single HTTP POST request
}

[!CAUTION] Mitigation for Batching Attacks:

  1. Disable HTTP Array Batching at the API Gateway or GraphQL engine middleware.
  2. Enforce a strict Max Alias Limit (e.g., maximum 5 aliases per query).
  3. Enforce cost analysis that multiplies complexity per alias executed.

2. gRPC & Protocol Buffers Security

gRPC uses HTTP/2 as its transport protocol and Protocol Buffers (Protobuf) as its binary serialization mechanism, offering performance advantages over REST/JSON. However, HTTP/2 framing and binary serialization present unique security considerations.

┌─────────────────────────────────────────────────────────────────────────────┐
│ gRPC SECURITY STACK │
├─────────────────────────────────────────────────────────────────────────────┤
│ Application Layer ──► Protobuf Messages (Field validation, sanitization) │
│ Context / Auth ──► Metadata Interceptors (JWT Bearer Token / OAuth2) │
│ Transport Layer ──► HTTP/2 Framing (Multiplexing, Flow Control, Headers) │
│ Encryption Layer ──► Mutual TLS (mTLS) with SAN / SPIFFE Verification │
└─────────────────────────────────────────────────────────────────────────────┘

A. gRPC Metadata Authentication Interceptors

In gRPC, authorization credentials (such as OAuth2 access tokens or JWTs) are transmitted inside gRPC Metadata (HTTP/2 header key-value pairs). Servers must enforce unary and streaming server interceptors to validate tokens before handling requests.

Go gRPC Unary Server Interceptor

package interceptors

import (
"context"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

// UnaryAuthInterceptor validates JWT in gRPC incoming metadata
func UnaryAuthInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {

md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "Missing metadata context")
}

values := md["authorization"]
if len(values) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "Missing authorization token")
}

tokenStr := strings.TrimPrefix(values[0], "Bearer ")
userClaims, err := validateJWTToken(tokenStr)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "Invalid or expired token: %v", err)
}

// Attach validated claims to context for downstream handlers
newCtx := context.WithValue(ctx, "userClaims", userClaims)
return handler(newCtx, req)
}

Java gRPC ServerInterceptor (Spring Boot / gRPC Java)

package com.appsec.grpc.interceptor;

import io.grpc.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

public class JwtServerInterceptor implements ServerInterceptor {

private static final Metadata.Key<String> AUTHORIZATION_METADATA_KEY =
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);

@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {

String authHeader = headers.get(AUTHORIZATION_METADATA_KEY);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
call.close(Status.UNAUTHENTICATED.withDescription("Missing Bearer token"), headers);
return new ServerCall.Listener<ReqT>() {};
}

String token = authHeader.substring(7);
try {
Claims claims = validateToken(token);
Context context = Context.current().withValue(USER_CLAIMS_KEY, claims);
return Contexts.interceptCall(context, call, headers, next);
} catch (Exception e) {
call.close(Status.UNAUTHENTICATED.withDescription("Invalid JWT Token"), headers);
return new ServerCall.Listener<ReqT>() {};
}
}
}

B. Mutual TLS (mTLS) for gRPC Channels

Because gRPC relies heavily on inter-service microservice communication, enforcing Mutual TLS (mTLS) ensures that both client and server authenticate each other's X.509 certificates before initiating gRPC streams.

Go gRPC Server with mTLS Configuration

package main

import (
"crypto/tls"
"crypto/x509"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

func LoadTLSCredentials() (credentials.TransportCredentials, error) {
// 1. Load Server Certificate & Key
serverCert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
return nil, err
}

// 2. Load CA Certificate to verify client certs
pemClientCA, err := os.ReadFile("ca.crt")
if err != nil {
return nil, err
}

certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(pemClientCA) {
return nil, err
}

// 3. Create TLS config requiring Mutual TLS (RequireAndVerifyClientCert)
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
MinVersion: tls.VersionTLS13, // Enforce TLS 1.3
}

return credentials.NewTLS(config), nil
}

func main() {
creds, _ := LoadTLSCredentials()
grpcServer := grpc.NewServer(grpc.Creds(creds))
// Register gRPC services...
}

C. HTTP/2 Specific Vector: Rapid Reset (CVE-2023-44487)

gRPC servers are vulnerable to HTTP/2 stream attacks, such as the HTTP/2 Rapid Reset vulnerability. Attackers send a stream of RST_STREAM frames immediately after opening HEADERS frames, causing high server CPU utilization without triggering traditional concurrent stream limiters.

[!IMPORTANT] HTTP/2 Mitigation:

  • Upgrade gRPC runtime libraries (grpc-go, grpc-java, grpc-node) to versions released after October 2023.
  • Configure MAX_CONCURRENT_STREAMS and limit the rate of incoming reset frames at the ingress load balancer (Envoy, Nginx, or AWS ALB).

Next Chapter: 04. Rate Limiting, Throttling & Auth →

Share this guide