From bea09c95b220dc1792b5b05b326cda17db987b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:53:21 +0000 Subject: [PATCH] fix(mcp-gateway): apply transport identity and framing rules to auxiliary headers --- packages/mcp-gateway/src/headers.ts | 25 ++++++++++++++ packages/mcp-gateway/src/index.test.ts | 48 ++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/mcp-gateway/src/headers.ts b/packages/mcp-gateway/src/headers.ts index 742e8985b59..07a0cc63214 100644 --- a/packages/mcp-gateway/src/headers.ts +++ b/packages/mcp-gateway/src/headers.ts @@ -22,6 +22,8 @@ const hopByHopHeaders = new Set([ 'upgrade', ]); +const framingHeader = 'content-length'; + const headerNamePattern = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/; const blockedExactHeaders = new Set([ @@ -102,6 +104,13 @@ const staticHeadersSchema = z.record(z.string(), headerValueSchema).superRefine( path: [name], }); } + if (normalized === framingHeader) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Static header cannot be a framing header', + path: [name], + }); + } } }); @@ -124,6 +133,20 @@ const auxiliaryHeadersSchema = z path: [name], }); } + if (isTransportIdentityHeader(normalized)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Auxiliary header cannot be transport identity metadata', + path: [name], + }); + } + if (normalized === framingHeader) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Auxiliary header cannot be a framing header', + path: [name], + }); + } } }); @@ -164,6 +187,8 @@ export function buildUpstreamHeaders(params: { if ( hopByHopHeaders.has(normalized) || isCredentialLikeHeader(normalized) || + isTransportIdentityHeader(normalized) || + normalized === framingHeader || blockedStaticNames.has(normalized) ) continue; diff --git a/packages/mcp-gateway/src/index.test.ts b/packages/mcp-gateway/src/index.test.ts index 32d56f4d96c..ab827d9c8c8 100644 --- a/packages/mcp-gateway/src/index.test.ts +++ b/packages/mcp-gateway/src/index.test.ts @@ -6,6 +6,7 @@ import { buildScopedConnectRootPath, buildUpstreamHeaders, parseScopedConnectPath, + parseAuxiliaryHeaders, parseStaticHeaders, isPublicIp, ProviderGrantBundleSchema, @@ -118,6 +119,50 @@ describe('provider grant schema', () => { }); }); +describe('auxiliary headers', () => { + test('rejects transport identity and framing headers', () => { + expect(() => parseAuxiliaryHeaders({ Host: 'example.com' })).toThrow( + 'Auxiliary header cannot be transport identity metadata' + ); + expect(() => parseAuxiliaryHeaders({ Forwarded: 'for=192.0.2.1' })).toThrow( + 'Auxiliary header cannot be transport identity metadata' + ); + expect(() => parseAuxiliaryHeaders({ 'X-Forwarded-For': '192.0.2.1' })).toThrow( + 'Auxiliary header cannot be transport identity metadata' + ); + expect(() => parseAuxiliaryHeaders({ 'X-Forwarded-Host': 'example.com' })).toThrow( + 'Auxiliary header cannot be transport identity metadata' + ); + expect(() => parseAuxiliaryHeaders({ 'CF-Connecting-IP': '192.0.2.1' })).toThrow( + 'Auxiliary header cannot be transport identity metadata' + ); + expect(() => parseAuxiliaryHeaders({ 'Content-Length': '10' })).toThrow( + 'Auxiliary header cannot be a framing header' + ); + expect(parseAuxiliaryHeaders({ 'X-Request-Id': 'request-1' })).toEqual({ + 'X-Request-Id': 'request-1', + }); + }); + + test('buildUpstreamHeaders drops transport identity and framing auxiliary headers', () => { + const headers = buildUpstreamHeaders({ + source: new Headers({ Accept: 'application/json' }), + auxiliaryHeaders: [ + ['Host', 'attacker.example'], + ['X-Forwarded-For', '192.0.2.1'], + ['Content-Length', '10'], + ['X-Request-Id', 'request-1'], + ], + }); + + expect(headers.get('host')).toBeNull(); + expect(headers.get('x-forwarded-for')).toBeNull(); + expect(headers.get('content-length')).toBeNull(); + expect(headers.get('x-request-id')).toBe('request-1'); + expect(headers.get('accept')).toBe('application/json'); + }); +}); + describe('static headers', () => { test('allows authorization but rejects transport identity headers', () => { expect(parseStaticHeaders({ Authorization: 'Bearer test' })).toEqual({ @@ -132,6 +177,9 @@ describe('static headers', () => { expect(() => parseStaticHeaders({ 'CF-Connecting-IP': '203.0.113.1' })).toThrow( 'Static header cannot be transport identity metadata' ); + expect(() => parseStaticHeaders({ 'Content-Length': '10' })).toThrow( + 'Static header cannot be a framing header' + ); expect( parseStaticHeaders({ 'CF-Access-Client-Id': 'client-id',