Topic: mcp server grpc security
MCP server gRPC security — TLS, reflection, and streaming
gRPC's binary framing and HTTP/2 transport introduce attack surfaces that don't exist in REST or GraphQL proxies. When an MCP tool wraps a gRPC service, four risks emerge: TLS certificate validation bypass (common in development configurations that get shipped to production), server reflection exposure (the gRPC equivalent of introspection — lists every service and method), metadata header injection (gRPC metadata travels in HTTP/2 headers and can be injected from LLM arguments), and unbounded streaming resource exhaustion (server-streaming RPCs that never terminate consume the MCP server's goroutines or Node.js event loop indefinitely).
Attack 1: TLS certificate validation bypass
gRPC connections to remote services must use TLS. The standard mistake in MCP servers that proxy internal or staging gRPC services is using grpc.credentials.createInsecure() because it "just works" during development — and the configuration never changes before production ship.
import * as grpc from '@grpc/grpc-js'
// WRONG: insecure connection — plaintext gRPC, no server identity validation
const client = new MyServiceClient(
'internal-api.example.com:50051',
grpc.credentials.createInsecure() // never use for non-localhost
)
// WRONG: TLS with certificate validation disabled — equivalent to no TLS
const insecureTls = grpc.credentials.createSsl(null, null, null, {
checkServerIdentity: () => undefined // disables CN/SAN validation — MITM possible
})
// CORRECT: system root CAs (for public endpoints)
const client = new MyServiceClient(
'api.example.com:443',
grpc.credentials.createSsl() // uses system root CA bundle, validates server cert
)
// CORRECT: custom CA (for internal services with private PKI)
import { readFileSync } from 'fs'
const caCert = readFileSync('/path/to/internal-ca.crt')
const client = new MyServiceClient(
'internal-api.example.com:50051',
grpc.credentials.createSsl(caCert) // validates against internal CA, does not disable CN check
)
Attack 2: Server reflection exposure
The gRPC reflection service (grpc.reflection.v1alpha.ServerReflection) allows any gRPC client to discover all services and methods on the server — the RPC equivalent of GraphQL introspection. When an MCP server enables reflection on its own gRPC server (if it exposes one) or proxies to an upstream that has reflection enabled, the LLM can discover methods the MCP tool was never intended to expose.
// WRONG: registering the reflection service on a production gRPC server
import { ReflectionService } from '@grpc/reflection'
import * as grpc from '@grpc/grpc-js'
const server = new grpc.Server()
server.addService(MyService.service, myServiceImpl)
// Only add reflection in development:
if (process.env.NODE_ENV !== 'production') {
const reflection = new ReflectionService(protoDescriptor)
reflection.addToServer(server)
}
// In production: reflection service is not registered, method discovery is not possible.
Attack 3: Metadata header injection
gRPC metadata is transmitted as HTTP/2 header frames and is commonly used for authentication tokens, request IDs, and routing information. If an MCP tool accepts metadata keys from LLM arguments and forwards them to the upstream gRPC service without validation, a prompt-injected LLM can inject arbitrary metadata — including overwriting authorization headers, adding tracing headers that bypass WAF rules, or injecting headers that trigger internal service routing.
// WRONG: forwarding raw LLM-supplied metadata to upstream gRPC
async function proxyRpcCall(methodName: string, args: any, llmMetadata: Record<string, string>) {
const metadata = new grpc.Metadata()
// LLM can inject: { "authorization": "Bearer attacker-token", "x-internal-bypass": "1" }
for (const [key, value] of Object.entries(llmMetadata)) {
metadata.set(key, value)
}
return client[methodName](args, metadata)
}
// CORRECT: allowlist metadata keys; inject server-side credentials separately
const ALLOWED_METADATA_KEYS = new Set(['x-request-id', 'accept-language'])
async function proxyRpcCall(methodName: string, args: any, llmMetadata: Record<string, string>) {
const metadata = new grpc.Metadata()
// Server-side credentials — never from LLM arguments
metadata.set('authorization', `Bearer ${process.env.INTERNAL_SERVICE_TOKEN}`)
// Allowlisted passthrough keys from LLM arguments
for (const [key, value] of Object.entries(llmMetadata)) {
if (ALLOWED_METADATA_KEYS.has(key)) metadata.set(key, value)
}
return client[methodName](args, metadata)
}
Attack 4: Unbounded streaming resource exhaustion
gRPC server-streaming RPCs can produce an unlimited number of messages. An MCP tool that wraps a streaming RPC without a message or time limit will consume memory and CPU indefinitely if the upstream sends a large response — or if a prompt-injected LLM triggers a high-cardinality streaming call intentionally.
// WRONG: collecting all messages without limit
async function streamRpc(args: any): Promise<any[]> {
const call = client.streamingMethod(args)
const results: any[] = []
return new Promise((resolve, reject) => {
call.on('data', msg => results.push(msg)) // no limit — could grow indefinitely
call.on('end', () => resolve(results))
call.on('error', reject)
})
}
// CORRECT: deadline + message limit + early cancellation
const MAX_STREAM_MESSAGES = 100
const STREAM_DEADLINE_MS = 10_000
async function streamRpc(args: any): Promise<any[]> {
const deadline = new Date(Date.now() + STREAM_DEADLINE_MS)
const call = client.streamingMethod(args, { deadline })
const results: any[] = []
return new Promise((resolve, reject) => {
call.on('data', msg => {
results.push(msg)
if (results.length >= MAX_STREAM_MESSAGES) {
call.cancel() // stop consuming; return what we have
resolve(results)
}
})
call.on('end', () => resolve(results))
call.on('error', err => {
if (err.code === grpc.status.CANCELLED) resolve(results) // our own cancel — not an error
else reject(err)
})
})
}
What SkillAudit checks
- Insecure credentials (
createInsecure) for non-localhost endpoints — HIGH; no TLS, susceptible to MITM - Reflection service registered in production build — WARN; schema disclosure
- Unvalidated metadata forwarding from tool arguments — HIGH; authentication bypass and header injection
- Streaming RPC without deadline or message limit — WARN; resource exhaustion under prompt injection
See also
- MCP server TLS configuration — TLS settings for HTTP-transport servers
- MCP server GraphQL security — parallel introspection and injection risks for GraphQL proxies
- MCP server OWASP Top 10 — resource exhaustion and injection in the full threat model
- Public audit corpus — gRPC and transport security findings
Check your gRPC-proxying server for TLS and metadata injection findings.
Run a free audit → How grading works →