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

See also

Check your gRPC-proxying server for TLS and metadata injection findings.

Run a free audit → How grading works →