DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

gRPC Security: The Authorization Model REST Scanners Cannot See

Your gRPC service has TLS, mTLS configured, and JWT middleware on every endpoint. A client opens a bidirectional stream, auth validates at handshake time, and everything that follows is authorized by inertia. That includes the 47 minutes after the token expired.

gRPC replaces REST's plaintext surface with a binary one, but the authorization model is the same: one check at connection time, nothing per message. The binary transport fools WAFs and security scanners while leaving the same privilege escalation paths open.

Server reflection is the Swagger UI you forgot to disable

The grpc.reflection.v1alpha service exposes your entire API surface to any client with grpcurl, without credentials, without source files. grpcurl -plaintext host:port list returns all service names in seconds. grpcurl describe <service> returns full .proto definitions, equivalent to a public OpenAPI spec.

The offensive value is identical to leaving Swagger UI open in production. An attacker enumerates services, discovers admin and internal methods, and calls endpoints the frontend never exposes. The workflow: enumerate services with list, inspect schemas with describe, identify unauthenticated endpoints, call them.

Reflection is enabled by default in most gRPC server implementations during development. Operators routinely leave it on in production. No authentication is required by the reflection API unless explicitly gated.

Disable reflection in production: grpc.NewServer() without reflection.Register(). If needed for debugging, restrict to internal CIDR via network policy. Validate via automated config audit.

CVE-2026-33186: one missing slash, CVSS 9.1, no privileges required

gRPC-Go before version 1.79.3 had a flaw in parsing the HTTP/2 :path pseudo-header. When a client sent Service/Method without the leading slash, the server routed the RPC correctly. The authorization interceptor chain, however, evaluated the raw non-canonical path.

Deny rules configured for /Service/Method did not match Service/Method. The fallback allow rule fired. The RPC executed without an authorization check.

CVE-2026-33186, CVSS 9.1 Critical (AV:N/AC:L/PR:N/UI:N/C:H/I:H/A:N). Remote exploitation, low complexity, no privileges, no user interaction required. IBM Fusion and DataDog Agent were among the affected services.

Custom interceptors using info.FullMethod or grpc.Method(ctx) were equally affected. The bug was not in the individual interceptor. It was in the path normalization step before dispatch to the interceptor chain. Any gRPC-Go server with path-based authorization before 1.79.3 was vulnerable.

gRPC-Go 1.79.3 fixes this by rejecting paths without a leading slash with codes.Unimplemented before interceptor dispatch. No workaround exists for earlier versions besides upgrading. A default-deny policy would have limited the impact regardless of the normalization bug.

Streaming RPCs validate once and trust forever

Unary RPC: one request, one auth check, one response. Server-side streaming, client-side streaming, and bidirectional streaming: the interceptor fires on the initial call metadata only. No check per subsequent message.

A JWT with a 15-minute expiry issued at stream open can authorize messages for hours on the same open bidirectional stream. A token revoked before a privilege change event stays valid for the stream's lifetime. The gRPC protocol has no server-initiated reauth primitive.

Most interceptor implementations call token validation once at stream initialization via grpc.GetOutgoingContext(). No native mechanism exists for per-message re-validation. The OWASP gRPC Security Cheat Sheet identifies long-lived tokens as a direct risk; streaming makes any token effectively long-lived.

The fix requires per-message authorization logic. Use a wrapper on stream.RecvMsg() that re-validates the token on each received frame. Set context deadlines to cancel streams with expired tokens.

Protobuf unknown fields carry injection payloads past the schema

Proto3 retained unknown fields by default since version 3.5. The original proto3 dropped unknown fields entirely; the behavior changed in v3.5. Unknown fields are field numbers and wire types not defined in the current schema: they pass through the parser silently.

Server-side validation runs on the deserialized struct. Unknown fields never appear in the struct and never trigger validation errors. Injection payloads can ride unknown fields adjacent to valid parameters, bypassing business logic that only validates known fields.

Field number reuse across schema versions creates type confusion between services. Field 5 was email in v1 and is is_admin in v2. A client on the v1 schema sends a value for the old field 5; the v2 server interprets it as is_admin. The result is privilege escalation via schema version mismatch.

Use proto.UnmarshalOptions{DiscardUnknown: true} for all internet-facing handlers. Validate schema version compatibility via interceptor. REST APIs validate against JSON schema at the framework level; in gRPC, validation is opt-in via protoc-gen-validate.

HTTP/2 Rapid Reset made gRPC a DDoS amplifier

CVE-2023-44487, CVSS 7.5 High (AV:N/AC:L/PR:N/UI:N/C:N/I:N/A:H). A client sends a HEADERS frame to open an HTTP/2 stream, the server allocates the stream, the client immediately sends RST_STREAM. The server tears down the stream and attempts a response. The attacker cycles millions of streams per second.

The largest DDoS attacks on record used this vector between August and October 2023. Google reported 398 million requests per second; Cloudflare, 201 million; AWS, 155 million. gRPC multiplexes all RPCs over HTTP/2 streams. Every internet-exposed gRPC service was vulnerable.

In gRPC-Go, GHSA-m425-mq94-257g documents that the server launched more concurrent handlers than the configured MaxStreamLimit. CVE-2023-4785 is a separate issue: TCP connection exhaustion in gRPC C++, Python, and Ruby via connection flooding. CVE-2024-7246 adds HPACK table poisoning via HTTP/2 proxy, leaking other clients' header keys across the shared pool.

The fix: gRPC-Go >= 1.56.3, 1.57.1, or 1.58.3 for CVE-2023-44487. Set grpc.MaxConcurrentStreams(100) on the server. Add TCP-level rate limiting before the gRPC layer.

gRPC-gateway and gRPC-Web paste the REST attack surface back on top

gRPC-gateway translates HTTP/1.1 REST calls to gRPC. grpc-web does the same for browser clients via Envoy. The translation layer reintroduces CORS misconfig, header injection, path traversal, and HTTP verb confusion. None of those attacks pass through the gRPC interceptor chain.

The JSON body in a gRPC-gateway request passes through JSON parsing before conversion to protobuf. JSON injection attacks apply at the gateway layer, before any gRPC interceptor. CVE-2023-32732, CVSS 5.3: malformed base64 in -bin headers terminates HTTP/2 proxy-to-server connections, disrupting the shared pool.

CVE-2024-7246: a client via HTTP/2 proxy poisons the shared HPACK table, leaking other clients' header keys. Fixed in gRPC 1.58.3-1.65.4. Envoy gRPC-Web does not enforce the Origin header by default; the CORS policy on the Envoy filter must be set explicitly.

Treat the gateway REST surface as a separate application with its own auth middleware. gRPC interceptor chains do not cross the proxy boundary.

Defense: controls matched to the actual attack surface

Reflection: disable reflection.Register() in production. If needed for debugging, restrict to internal CIDR via network policy and validate in automated config audits.

Patches: gRPC-Go >= 1.79.3 for CVE-2026-33186, >= 1.56.3 for CVE-2023-44487. Check transitive dependencies, not just direct project dependencies.

Streaming: add per-message reauth via a wrapper on stream.RecvMsg() that calls the token validator on each received frame. Set context deadlines to invalidate streams after a time limit.

Unknown fields: proto.UnmarshalOptions{DiscardUnknown: true} for all internet-facing handlers. Use protoc-gen-validate for declarative field validation.

Concurrency: grpc.MaxConcurrentStreams(100) plus TCP rate limiting upstream.

Gateway: apply REST security controls at the gateway (CORS, input validation, rate limiting) independent of the gRPC interceptor chain.

The MAGO team tool (mago.team) enumerates exposed gRPC reflection endpoints during API surface mapping. It flags services that respond to unauthenticated ServerReflectionInfo calls and identifies streaming RPCs that lack per-message authorization.

The gRPC binary frame is not a security boundary. The authorization interceptor is. And it fires once per call, not once per message.

Top comments (0)