gRPC API Security Testing: Reflection, grpcurl, Protobuf Tampering, Auth Bypass, and Fuzzing
gRPC has quietly become the backbone of internal microservice traffic and an increasing share of public-facing mobile and IoT APIs. It rides on HTTP/2, serializes messages with Protocol Buffers, and exposes strongly-typed service contracts instead of loose JSON. That contract-first design makes gRPC efficient and pleasant to build against — but it also creates a security testing surface that most web app methodologies handle poorly. Burp's default proxy does not understand HTTP/2 framed protobuf, ZAP cannot replay a unary call, and a tester who treats the wire bytes as opaque will miss authorization flaws hiding one field deep.
This guide walks through testing gRPC services the way an experienced pentester actually does it on an authorized engagement: enumerate the service surface (ideally via reflection), recover or reconstruct the .proto contract, drive calls with grpcurl, tamper with protobuf fields and metadata, attack authentication and authorization, and fuzz the message schema. Everything below assumes you have written permission to test the target.
How gRPC Differs from REST (and Why It Matters)
Before attacking anything, internalize the structural differences, because they dictate your tooling:
- Transport is HTTP/2, always multiplexed. Each call is a stream; method routing happens via the
:pathpseudo-header as/package.Service/Method. An intercepting proxy that only speaks HTTP/1.1 will not see your calls at all. - The body is binary Protobuf, length-prefixed. Each message frame is a 1-byte compression flag, a 4-byte big-endian length, then the serialized message. There is no human-readable JSON to eyeball.
- The schema is the API. Field names, types, and even authorization assumptions live in
.protofiles. If you can obtain the schema, you effectively have the API documentation. - Four call types: unary, server-streaming, client-streaming, and bidirectional streaming. Streaming endpoints often have weaker rate limiting and quota enforcement than unary ones.
- Status codes are gRPC codes, not HTTP codes. A call can return HTTP 200 with a trailer
grpc-status: 7 (PERMISSION_DENIED). Authorization signals live in trailers, so watch them, not the HTTP status line.
Discovering the Service Surface with Server Reflection
The single most valuable misconfiguration on a gRPC target is enabled server reflection. Reflection is a built-in service (grpc.reflection.v1alpha.ServerReflection) that lets a client ask the server to describe its own services, methods, and message types at runtime. It is the gRPC equivalent of leaving Swagger UI exposed in production — convenient for developers, a gift to an attacker.
grpcurl is the standard reconnaissance tool. List services, then drill into a service and a method:
# List all exposed services (works only if reflection is enabled)
grpcurl -plaintext target.internal:50051 list
# Expected output:
# grpc.reflection.v1alpha.ServerReflection
# shop.OrderService
# shop.AdminService
# Enumerate methods of a service
grpcurl -plaintext target.internal:50051 list shop.OrderService
# Describe a method's input/output message types
grpcurl -plaintext target.internal:50051 describe shop.OrderService.GetOrder
# Describe a message type to see every field, including ones the UI never sends
grpcurl -plaintext target.internal:50051 describe shop.GetOrderRequest
Use -plaintext for cleartext h2c, drop it (and let TLS negotiate) for TLS endpoints. The describe output of message types is where hidden attack surface appears: is_admin flags, internal_notes, tenant_id, or debug fields that the official client never populates but the server still reads. That is mass assignment waiting to happen.
When Reflection Is Disabled: Recovering the Schema
Mature deployments disable reflection in production. You are not stuck — you just have to reconstruct the contract:
- Pull
.protofiles from the client. Mobile apps, SPAs, and CLI binaries embed message descriptors. Decompile an Android APK and grep forFileDescriptorProtoor generated stub class names; many gRPC-Web apps ship the compiled descriptor in JS bundles. - Use the descriptor set if you can find one. Build pipelines often produce a
descriptor.pb(FileDescriptorSet). If it leaks via a misconfigured artifact store or source map, feed it to grpcurl with-protoset descriptor.pbto call methods without reflection. - Capture real traffic and infer fields. With the client's own traffic decrypted (pinning bypass on mobile, or a trusted CA in a lab), you can read the protobuf wire format directly.
The protobuf wire format is self-describing enough to parse blindly. Field tags encode a field number and wire type in a varint; protoc --decode_raw turns raw bytes into a field-number/value tree even without the schema:
# Decode an intercepted protobuf body with no .proto file
cat captured_message.bin | protoc --decode_raw
# Sample output: field 1 is a varint (likely an ID), field 2 a string
# 1: 4815
# 2: "[email protected]"
# 3: 1
# Once you know the numbers, calling with -protoset and explicit field data:
grpcurl -protoset shop.pb -d '{"orderId": 4815}' \
target.internal:50051 shop.OrderService.GetOrder
Field 3 above (a boolean-looking 1) is exactly the kind of value worth flipping. When you reconstruct field numbers from captured bytes, treat base64/hex decoding as a routine step — gRPC-Web frames base64-encode the length-prefixed message, and you will need to decode them before you can read or rebuild the protobuf payload.
Protobuf Tampering and IDOR/BOLA
Because gRPC is contract-first, developers frequently assume the client only sends well-formed, expected values — and skip server-side authorization on individual fields. This makes Broken Object Level Authorization (BOLA/IDOR) and mass assignment the highest-yield bugs on most gRPC engagements.
Test object-level authz the same way you would for REST: authenticate as a low-privilege user, then reference objects you should not own.
# Authenticated as user 4815 — request another tenant's order
grpcurl -H "authorization: Bearer $LOW_PRIV_TOKEN" \
-d '{"orderId": 9999}' \
target.internal:50051 shop.OrderService.GetOrder
# Mass assignment: send fields the official client never sends.
# 'role' and 'isVerified' came straight out of the describe output.
grpcurl -H "authorization: Bearer $LOW_PRIV_TOKEN" \
-d '{"name": "alice", "role": "admin", "isVerified": true}' \
target.internal:50051 shop.UserService.UpdateProfile
Other field-level tampering worth trying: negative quantities and prices (business-logic / integer overflow), oversized strings to probe length validation, supplying tenant_id values from a different organization, and toggling enum values to undefined indices. Because protobuf has no required-by-default fields in proto3, omitting a field sends its zero value — sometimes that silently bypasses a check that only fires on non-zero input. The mindset overlaps heavily with GraphQL testing; if you want a parallel treatment of schema-driven authorization flaws, see our GraphQL security guide.
Attacking Authentication and Metadata
gRPC carries auth in metadata — key/value pairs sent as HTTP/2 headers. The most common scheme is a bearer token in the authorization header, frequently a JWT. Standard token attacks apply directly:
- Missing or weak verification. Strip the
authorizationheader entirely and re-issue the call. Many internal services trust the network perimeter and never validate tokens on east-west traffic. - JWT algorithm and signature flaws. Test
alg: none, key confusion (RS256→HS256), and expired-token acceptance. Decode and re-sign candidate tokens with the JWT Decoder & Builder before replaying them through grpcurl. - Custom metadata trust. Internal services often trust headers like
x-user-id,x-tenant-id, orx-internal: truethat an upstream gateway is supposed to set. If the backend is reachable directly, spoof them.
# Spoof a header an API gateway normally injects
grpcurl -H "x-user-id: 1" -H "x-internal-request: true" \
-d '{}' target.internal:50051 shop.AdminService.ListAllUsers
# Probe channel security: does the server accept cleartext?
grpcurl -plaintext target.internal:50051 list
# A successful response over -plaintext on a 'production' port = unencrypted gRPC
Always inspect the response trailers. A call returning grpc-status: 0 (OK) after you removed authentication is a finding; grpc-status: 16 (UNAUTHENTICATED) or 7 (PERMISSION_DENIED) means the control held.
Fuzzing gRPC Methods
The strong typing of protobuf narrows the input space, which is good for structured fuzzing. Because you know each field's type from the schema, you can generate type-aware mutations rather than blind byte flipping. Effective targets are length-prefix abuse, deeply nested messages (recursion / stack exhaustion), huge repeated fields (memory amplification), and malformed varints.
# Quick differential fuzz of a numeric field with a shell loop
for v in -1 0 2147483648 9223372036854775807 99999999999999999999; do
echo "== quantity=$v =="
grpcurl -H "authorization: Bearer $TOKEN" \
-d "{\"sku\": \"A1\", \"quantity\": $v}" \
target.internal:50051 shop.CartService.AddItem
done
# Compression-bomb / decompression DoS probe: a tiny gzip frame that
# expands enormously is a classic resource-exhaustion test for the codec.
For coverage-guided work, protobuf-mutator (libprotobuf-mutator with libFuzzer) mutates messages while keeping them schema-valid, and tools like ffuf-style wrappers or custom Python with the grpc library let you script high-volume campaigns. Watch streaming endpoints especially: client-streaming and bidi calls can let you hold a connection open and send unbounded messages, which frequently bypasses per-request rate limits.
Remediation and Secure Design
Defenses for gRPC are concrete and largely about not trusting the contract to enforce security:
- Disable server reflection in production. Gate it behind a build flag or an authenticated admin network only. Treat an exposed reflection service as a critical information-disclosure finding.
- Enforce TLS everywhere, including east-west. Use mTLS for service-to-service auth so a leaked bearer token is not the only thing standing between an attacker and your backends. Reject
h2c/plaintext on production listeners. - Authorize every field and every object, server-side. Never rely on the client to omit privileged fields. Validate
tenant_id/ownership on every read and write, and ignore client-supplied role or trust fields entirely. - Validate tokens on every hop. Use an auth interceptor that runs before business logic, verifies signature, algorithm, audience, and expiry, and never trusts gateway-injected headers unless the channel is mutually authenticated.
- Set message-size and recursion limits. Configure
MaxRecvMsgSize, cap decompression ratios, bound repeated-field counts, and apply timeouts and rate limits to streaming RPCs, not just unary ones. - Centralize controls in interceptors. Put authentication, authorization, input validation, and logging in unary and stream interceptors so no method ships without them.
For ongoing testing across all of your service types, the API Security Studio helps you organize OWASP API Top 10 checks alongside the protobuf-specific tampering and metadata abuse described here. The reconnaissance pattern is the same as any other API engagement — recover the contract, enumerate the surface, then attack authorization one field at a time — but gRPC rewards testers who are willing to work at the protobuf wire level rather than treating the bytes as a black box.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides