From 2b77c8539f264eaec564cecdc52259c371b7b776 Mon Sep 17 00:00:00 2001 From: Jacopo Cappellato Date: Mon, 1 Dec 2025 18:43:34 +0100 Subject: [PATCH] Modify Origin header validation in validateRequestHeaders (streamableHttp.ts and sse.ts) to allow requests without an Origin, as they are not relevant to server DNS rebinding protection. --- src/server/sse.test.ts | 21 +++++++++++++++++++++ src/server/sse.ts | 2 +- src/server/streamableHttp.test.ts | 23 +++++++++++++++++++++++ src/server/streamableHttp.ts | 2 +- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/server/sse.test.ts b/src/server/sse.test.ts index b95490c13..b752790cf 100644 --- a/src/server/sse.test.ts +++ b/src/server/sse.test.ts @@ -547,6 +547,27 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); }); + it('should accept requests without origin headers', async () => { + const mockRes = createMockResponse(); + const transport = new SSEServerTransport('/messages', mockRes, { + allowedOrigins: ['http://localhost:3000', 'https://example.com'], + enableDnsRebindingProtection: true + }); + await transport.start(); + + const mockReq = createMockRequest({ + headers: { + 'content-type': 'application/json' + } + }); + const mockHandleRes = createMockResponse(); + + await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); + + expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202); + expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); + }); + it('should reject requests with disallowed origin headers', async () => { const mockRes = createMockResponse(); const transport = new SSEServerTransport('/messages', mockRes, { diff --git a/src/server/sse.ts b/src/server/sse.ts index a603f9f8b..abe969b3c 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -79,7 +79,7 @@ export class SSEServerTransport implements Transport { // Validate Origin header if allowedOrigins is configured if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) { const originHeader = req.headers.origin; - if (!originHeader || !this._options.allowedOrigins.includes(originHeader)) { + if (originHeader && !this._options.allowedOrigins.includes(originHeader)) { return `Invalid Origin header: ${originHeader}`; } } diff --git a/src/server/streamableHttp.test.ts b/src/server/streamableHttp.test.ts index d2a29ac9e..9cdefe090 100644 --- a/src/server/streamableHttp.test.ts +++ b/src/server/streamableHttp.test.ts @@ -2678,6 +2678,29 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { const body = await response.json(); expect(body.error.message).toBe('Invalid Origin header: http://evil.com'); }); + + it('should accept requests without origin headers', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedOrigins: ['http://localhost:3000', 'https://example.com'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + // Should pass even with no Origin headers because requests that do not come from browsers may not have Origin and DNS rebinding attacks can only be performed via browsers + expect(response.status).toBe(200); + }); }); describe('enableDnsRebindingProtection option', () => { diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index 1e04390cf..0473c4645 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -228,7 +228,7 @@ export class StreamableHTTPServerTransport implements Transport { // Validate Origin header if allowedOrigins is configured if (this._allowedOrigins && this._allowedOrigins.length > 0) { const originHeader = req.headers.origin; - if (!originHeader || !this._allowedOrigins.includes(originHeader)) { + if (originHeader && !this._allowedOrigins.includes(originHeader)) { return `Invalid Origin header: ${originHeader}`; } }