DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

gRPC Server Reflection: The Unauthenticated API Catalog in Your Production Service

gRPC Server Reflection: The Unauthenticated API Catalog in Your Production Service

A single command, grpcurl -plaintext api.internal:50051 list, returns every service your cluster runs, including the ones you never meant to expose. The .proto files never left your repository. The schema just did.

Teams enable gRPC reflection during development for grpcurl convenience, then ship it to production without environment gating. That hands attackers a self-documenting proto catalog readable from a single unauthenticated RPC.

Reflection is the server's descriptor pool exposed as an RPC

gRPC reflection is not a convenience wrapper. It is a direct interface to the server's in-process proto descriptor pool. Responses are serialized FileDescriptorProto messages that fully reconstruct any .proto file the server compiled in.

The grpc.reflection.v1alpha.ServerReflection service exposes one bidirectional streaming RPC: ServerReflectionInfo. Requests query the pool by symbol, file name, or extension. Responses are serialized FileDescriptorProto bytes, the same representation protoc produces.

Clients like grpcurl, evans, and Postman reconstruct the full .proto graph from those bytes without any source files. The v1alpha package is marked deprecated in the canonical proto spec; the stable successor is v1, but v1alpha remains the dominant deployed version in production.

One line of code, no environment gate, shipped to production

Reflection requires explicit registration, which looks like an opt-in control. Developers add reflection.Register(server) early in development to use grpcurl without .proto files. That line travels unchanged to production because removing it breaks the local tooling workflow.

No gRPC library (Go, Java, Python, Node) enables reflection by default. Gitpod merged PR #10060 in May 2022. The team explicitly enabled reflection on their public API server with the rationale: "makes the API more discoverable and self documenting for consumers." No reviewer raised a security objection.

OWASP recommends environment-gating, signaling this gate is frequently absent in production deployments. grpcurl and evans fail silently without reflection when no .proto files are provided, creating strong development workflow pressure to leave reflection enabled permanently.

Thirty seconds from zero to complete schema

With reflection enabled, an attacker with no prior knowledge reaches full service enumeration and ready-to-invoke method schemas in under 5 commands. None require credentials.

# 1. List all services
grpcurl -plaintext api.target:50051 list

# 2. Describe a full service
grpcurl -plaintext api.target:50051 describe admin.v1.AdminService

# 3. Filter privileged namespaces
grpcurl -plaintext api.target:50051 list | grep -E '(admin|debug|internal|management)'

# 4. Full offline schema dump
grpcurl -plaintext api.target:50051 describe > schema.txt 2>&1

# 5. Direct admin method invocation
grpcurl -plaintext -d '{"user_id":"1"}' api.target:50051 admin.v1.AdminService.ListAllUsers
Enter fullscreen mode Exit fullscreen mode

Step 4's dump eliminates any need for further server contact during exploitation. The full schema, with service, method, field, and type names, is already offline.

Authentication guards the data plane; reflection rides a separate track

gRPC authentication interceptors apply to registered business methods. The reflection service registers itself independently and, without specific interception, bypasses the same middleware protecting your UserService and PaymentService.

go-grpc-middleware issue #244 documents this gap. A developer could not exclude ServerReflectionInfo from auth interceptors. The interceptor signature does not expose the method name at the auth function's call site.

When mTLS applies at the service mesh level, the gRPC server may also listen on an external port. Reflection is accessible to anything reaching that port, regardless of client certificate status. Schema retrieved without authentication enables crafting precisely formed requests against authenticated endpoints.

What the schema hands an attacker that REST APIs do not

In REST APIs, undocumented endpoints require guessing URL paths and parameter shapes. gRPC reflection delivers every method name, every field name, every enum value, and every nested type. That includes methods the frontend never calls and fields marked deprecated but still processed server-side.

Internal namespaces like AdminService, DebugService, and InternalMetricsService appear alongside public services. Reflection does not distinguish public from internal scope. The Protobuf [deprecated = true] annotation is not a security control: the server continues deserializing and applying deprecated fields, and reflection exposes them. Enum values expose authorization tiers; a role field with values VIEWER, EDITOR, ADMIN tells an attacker exactly what to forge in a token.

Detection: passive fingerprinting finds gRPC before you try reflection

gRPC endpoints are identifiable passively via TLS ALPN negotiation and port conventions, before any application-layer probe. A single reflection call confirms the full attack surface without triggering most API security monitoring.

gRPC over HTTP/2 advertises h2 in ALPN. A TLS ClientHello scan on ports 443, 50051, and 8443 that responds with h2 identifies a candidate gRPC endpoint passively. Port 50051 is the conventional gRPC default, and scanners search for it in cloud asset inventories and Kubernetes service exports.

nmap -p 50051 --script grpc-reflect target
Enter fullscreen mode Exit fullscreen mode

The MAGO Intel tool (intel.mago.team) identifies gRPC-enabled services via passive TLS ALPN fingerprinting and port 50051 exposure, flagging reflection-enabled endpoints without active probing.

When reflection is disabled, Adversis's blind enumeration technique recovers the service map through error-response differentials. The "unknown service X" vs "unknown method Y for service X" distinction separates real from non-existent services via wordlist. The entire process runs over a single HTTP/2-multiplexed TCP connection.

Fix: gate by environment, auth the reflection plane, separate the debug surface

Disabling reflection in production solves the immediate problem. Environment gating and reflection-specific auth prevent regression when the next developer re-enables it locally and promotes to production.

// Environment gate: prod-safe without breaking local tooling
if os.Getenv("GRPC_REFLECTION") == "true" {
    reflection.Register(grpcServer)
}
Enter fullscreen mode Exit fullscreen mode

For teams requiring reflection in production with access control, a reflection-specific interceptor is the solution. It authenticates calls to the /grpc.reflection.v1alpha.ServerReflection/ path using an internal service account token, separate from the user-facing auth flow.

// Interceptor targeting the reflection service
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    if strings.HasPrefix(info.FullMethod, "/grpc.reflection.") {
        if err := requireInternalToken(ctx); err != nil {
            return nil, status.Error(codes.Unauthenticated, "reflection requires internal token")
        }
    }
    return handler(ctx, req)
}
Enter fullscreen mode Exit fullscreen mode

Port separation isolates the debug surface: bind reflection to a second server instance on 127.0.0.1:50052, not exposed through the load balancer. Envoy and Istio can filter /grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo at the network layer, independent of application code. A CI check closes the loop: grep for reflection.Register without an adjacent environment guard, failing the build if reflection is registered unconditionally.

The operational fix is one if statement. The architectural fix is treating the reflection service as a privileged endpoint. Any client reaching the gRPC port can read your internal API spec. That includes the attacker who just found your LoadBalancer in a Shodan scan.

Top comments (0)