7 min read

gRPC Security Testing

gRPC is a high-performance RPC framework using Protocol Buffers (protobuf) over HTTP/2. It's increasingly used for microservice communication and public APIs. gRPC services often lack the same security scrutiny as REST APIs, making them a valuable attack surface.

Information

gRPC traffic is binary (protobuf), not JSON — you need specialized tools. Burp Suite has no native gRPC or Protobuf support, so inspecting it in Burp means a third-party extension: bRPC-Web (gRPC-Web request/response disassembly and tampering) or burp-protobuf-decoder (bare Protobuf). For direct gRPC work use grpcurl or grpcui.

Frameworks: WSTG n/a (v4.2 covers GraphQL only) · ASVS 5.0 V8.2.1, V8.2.2 (also V13.2.1) · CWE-306, CWE-287 · T1190

How gRPC Differs From REST

gRPC replaces the URL-and-verb model with a method name — package.Service/Method — encoded as protobuf over HTTP/2. That changes what a proxy can see: there are no path parameters to fuzz, no JSON keys in the body, and the response status arrives in HTTP/2 trailers rather than the status line. The method name is the whole routing surface, and the message schema, not the URL, decides which fields exist.

The precondition for most tests is a schema. Without reflection you need the .proto files; without those you are guessing at field numbers. Reflection is the easiest way in, and its presence in production is itself worth reporting. A reachable HTTP/2 port gets you a connection, not a conversation.

Service Discovery & Enumeration

grpcurl list prints every service the server exposes, list <service> prints its methods, and describe <method> prints the request and response types. A positive result is a production service returning a full method list you were never given documentation for — that is your attack-surface map. A benign result is Unimplemented for the reflection service itself, which means reflection is off rather than that no services exist. If it is off, recover the schema from a mobile package or a public repository, or capture it with -proto-out-dir on a development deployment and replay it with -import-path and -proto.

grpc-enumeration.sh
# Check if gRPC reflection is enabled (equivalent to auto-discovery):
grpcurl -plaintext target.com:50051 list
# Output: lists all available services

grpcurl -plaintext target.com:50051 describe
# Output: describes all services and their methods

# List methods for a specific service:
grpcurl -plaintext target.com:50051 list com.example.UserService
# Output:
# com.example.UserService.GetUser
# com.example.UserService.ListUsers
# com.example.UserService.CreateUser
# com.example.UserService.DeleteUser

# Describe a specific method (shows request/response types):
grpcurl -plaintext target.com:50051 describe com.example.UserService.GetUser

# If reflection is disabled, obtain .proto files:
# - Check for exposed .proto files on web servers
# - Extract from mobile app APK/IPA
# - Reverse engineer from captured traffic with protobuf-inspector

Authentication Testing

Two authentications are in play and they are easy to conflate. Transport authentication — mTLS from a service mesh — proves that a workload connected, not which end user it acts for. Message authentication is the metadata credential, authorization: Bearer … passed with -H and checked by an interceptor. A mesh that enforces mTLS without per-method authorisation lets any workload in the cluster call any method: a confused deputy, not a missing login. The status code separates the cases — Unauthenticated means the credential was rejected, PermissionDenied means it was accepted and the caller lacks the role.

grpc-auth-testing.sh
# Call methods without authentication:
grpcurl -plaintext target.com:50051 \
  com.example.UserService/ListUsers
# If it returns data → no auth required!

# Test with stolen/expired tokens:
grpcurl -plaintext \
  -H 'Authorization: Bearer EXPIRED_TOKEN' \
  target.com:50051 com.example.UserService/ListUsers

# Test admin methods with regular user token:
grpcurl -plaintext \
  -H 'Authorization: Bearer REGULAR_USER_TOKEN' \
  -d '{"user_id": "admin"}' \
  target.com:50051 com.example.AdminService/GetConfig

# Test BOLA (change user_id to another user):
grpcurl -plaintext \
  -H 'Authorization: Bearer USER_A_TOKEN' \
  -d '{"user_id": "user_b_id"}' \
  target.com:50051 com.example.UserService/GetUser

Server-Streaming and Resource Abuse

Server-streaming methods are the resource-exhaustion surface. A stream has no fixed response size, so a request that sets a high page_size, omits a limit, or subscribes to a watch keeps returning data until the server or the client stops. -max-time bounds your call in seconds; without it a forgotten stream fills your terminal and the server's egress budget. A positive result is a stream carrying records you are not entitled to, or one that never terminates on a bounded resource. A benign result is a bounded page with a next_page_token.

grpc-streaming.sh
# Bound a server-streaming call so a watch/list method cannot run forever
grpcurl -plaintext -max-time 10 \
  -d '{"page_size": 100000}' \
  target.com:50051 com.example.UserService/ListUsers

# Keep a long-lived stream through an idle proxy for the full window
grpcurl -plaintext -max-time 60 -keepalive-time 20 \
  target.com:50051 com.example.EventService/Watch

Injection & Fuzzing

Protobuf types constrain injection more than JSON does: an int32 cannot carry a quote and a bool cannot carry a comment sequence, which removes whole payload classes and concentrates the test on string fields and on fields the server later interpolates into a query or a shell. Sending a JSON string where the schema expects a number is a separate test — it probes whether the server validates before conversion.

grpc-injection-fuzzing.sh
# Inject SQL/NoSQL via gRPC message fields:
grpcurl -plaintext \
  -d '{"username": "admin\u0027 OR 1=1 --", "password": "x"}' \
  target.com:50051 com.example.AuthService/Login

# Inject into search/filter fields:
grpcurl -plaintext \
  -d '{"query": "test\" OR \"1\"=\"1"}' \
  target.com:50051 com.example.SearchService/Search

# Test with oversized payloads:
python3 -c "import json; print(json.dumps({'name': 'A'*100000}))" | \
  grpcurl -plaintext -d @ target.com:50051 com.example.UserService/CreateUser

# Test with unexpected types (send string where number expected):
grpcurl -plaintext \
  -d '{"user_id": "not_a_number"}' \
  target.com:50051 com.example.UserService/GetUser

# Test with negative/zero values:
grpcurl -plaintext \
  -d '{"amount": -1000, "to_account": "attacker"}' \
  target.com:50051 com.example.PaymentService/Transfer

Reading the Result

Read the trailers, not the HTTP status. A gRPC call returns HTTP 200 with grpc-status: 7 when permission is denied and grpc-status: 16 when the credential is missing, so a 200 in your proxy history is not a success. grpcurl -vv prints the response headers and trailers, which is what belongs in the report.

Testing Checklist

  • ☐ 1. Check if gRPC reflection is enabled (information disclosure)
  • ☐ 2. Enumerate all services and methods
  • ☐ 3. Test each method without authentication
  • ☐ 4. Test authorization — can user A access user B's data?
  • ☐ 5. Test injection in all string fields
  • ☐ 6. Test for insecure transport (plaintext gRPC without TLS)
  • ☐ 7. Test with grpcui for interactive exploration
  • ☐ 8. Bound every streaming call with -max-time and check whether the server enforces its own limit
  • ☐ 9. Read the response trailers — grpc-status: 7 is a denial, not a success

Evidence Collection

Reflection Output: Full service listing showing exposed methods

Auth Bypass: grpcurl command and response showing unauthenticated access

Method Inventory: Every method name with its request/response type, and which ones answered without a metadata credential

Status Trailers: <code>grpc-status</code> and <code>grpc-message</code> from the trailer block for the denied and allowed calls

CVSS Range: Reflection disclosure: 3.7–5.3 | Auth bypass: 8.1–9.8

Remediation

  • • Disable reflection in production: Only enable gRPC reflection in development environments.
  • • Enforce TLS: Always use TLS for gRPC transport (gRPC over HTTPS).
  • • Auth on every RPC: Implement per-method authentication and authorization interceptors.
  • • Input validation: Validate all protobuf message fields server-side.
  • • Rate limiting: Apply rate limiting to gRPC methods (many gateway tools support this).
  • • Authorise per method: Mesh mTLS authenticates the workload, not the caller — keep a per-method interceptor that checks the end-user identity carried in metadata.
  • • Cap stream size: Enforce a maximum page size and a maximum stream duration server-side instead of trusting the client to stop.

False Positive Identification

  • gRPC reflection enabled in dev: Server reflection is a development feature — confirm it's enabled in production before reporting.
  • Missing TLS on internal services: gRPC without TLS on internal networks may be a design choice if the network is trusted — note the risk but assess the network context.
  • Proto file in public repo: Proto definitions may be intentionally public (open-source projects) — check if the proto reveals non-public internal service methods.
  • Mesh mTLS mistaken for authorisation: A call that succeeds from one workload to another proves the mesh is working. It is only a finding if the end-user identity in metadata was never checked.
  • HTTP 200 on a denied call: gRPC carries its result in trailers, so a 200 with <code>grpc-status: 7</code> is a permission denial. Do not report it as an authorisation bypass.