-
Notifications
You must be signed in to change notification settings - Fork 31
Harden HTTP proxy header handling #440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,18 @@ const responses: Record<string, http.ServerResponse> = {}; | |
| const minPort = 55000; | ||
| const maxPort = 55025; | ||
|
|
||
| const blockedProxyResponseHeaders = new Set([ | ||
| 'connection', | ||
| 'keep-alive', | ||
| 'proxy-authenticate', | ||
| 'proxy-authorization', | ||
| 'te', | ||
| 'trailer', | ||
| 'trailers', | ||
| 'transfer-encoding', | ||
| 'upgrade', | ||
| ]); | ||
|
|
||
| const invocRequestEmitter = new EventEmitter(); | ||
|
|
||
| export async function waitForProxyRequest(invocationId: string): Promise<http.IncomingMessage> { | ||
|
|
@@ -39,8 +51,11 @@ const invocationIdHeader = 'x-ms-invocation-id'; | |
| export async function sendProxyResponse(invocationId: string, userRes: HttpResponse): Promise<void> { | ||
| const proxyRes = nonNullProp(responses, invocationId); | ||
| delete responses[invocationId]; | ||
| const connectionHeader = userRes.headers.get('connection'); | ||
| for (const [key, val] of userRes.headers.entries()) { | ||
| proxyRes.setHeader(key, val); | ||
| if (isAllowedProxyResponseHeader(key, connectionHeader)) { | ||
| proxyRes.setHeader(key, val); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Can you check RFC Compliant docs if we can preseves 'content-length'.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. \Content-Length\ is an end-to-end header, and dropping it may have compatibility impact for HEAD/304/206/download scenarios. I am leaving this thread open for discussion rather than changing it blindly. My original concern was mismatched framing when proxying streamed bodies; we should decide whether preserving it is preferred or whether only conflicting cases should be filtered.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So what's your recommendation?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My recommendation is to preserve
The original concern was about forwarding connection/framing controls through this proxy. This code copies user response headers to Node.js To address that without treating
I am not validating |
||
| } | ||
| proxyRes.setHeader(invocationIdHeader, invocationId); | ||
| proxyRes.statusCode = userRes.status; | ||
|
|
@@ -57,6 +72,23 @@ export async function sendProxyResponse(invocationId: string, userRes: HttpRespo | |
| proxyRes.end(); | ||
| } | ||
|
|
||
| export function isAllowedProxyResponseHeader(headerName: string, connectionHeader?: string | null): boolean { | ||
| const normalizedHeaderName = headerName.toLowerCase(); | ||
| return ( | ||
| !blockedProxyResponseHeaders.has(normalizedHeaderName) && | ||
| !getConnectionHeaderNames(connectionHeader).has(normalizedHeaderName) | ||
| ); | ||
| } | ||
|
|
||
| function getConnectionHeaderNames(connectionHeader?: string | null): Set<string> { | ||
| return new Set( | ||
| connectionHeader | ||
| ?.split(',') | ||
| .map((headerName) => headerName.trim().toLowerCase()) | ||
| .filter((headerName) => headerName.length > 0) | ||
| ); | ||
| } | ||
|
|
||
| function setCookies(userRes: HttpResponse, proxyRes: http.ServerResponse): void { | ||
| const serializedCookies: string[] = userRes.cookies.map((c) => { | ||
| let sameSite: true | false | 'lax' | 'strict' | 'none' | undefined; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Copyright (c) .NET Foundation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import 'mocha'; | ||
| import { expect } from 'chai'; | ||
| import { isAllowedProxyResponseHeader } from '../../src/http/httpProxy'; | ||
|
|
||
| describe('Http proxy', () => { | ||
| it('Blocks hop-by-hop response headers', () => { | ||
| const blockedHeaders = [ | ||
| 'connection', | ||
| 'keep-alive', | ||
| 'proxy-authenticate', | ||
| 'proxy-authorization', | ||
| 'te', | ||
| 'trailer', | ||
| 'trailers', | ||
| 'transfer-encoding', | ||
| 'upgrade', | ||
| ]; | ||
|
|
||
| for (const header of blockedHeaders) { | ||
| expect(isAllowedProxyResponseHeader(header), header).to.be.false; | ||
| expect(isAllowedProxyResponseHeader(header.toUpperCase()), header).to.be.false; | ||
| } | ||
| }); | ||
|
|
||
| it('Allows end-to-end response headers', () => { | ||
| const allowedHeaders = ['content-type', 'cache-control', 'set-cookie', 'x-frame-options', 'content-length']; | ||
|
|
||
| for (const header of allowedHeaders) { | ||
| expect(isAllowedProxyResponseHeader(header), header).to.be.true; | ||
| } | ||
| }); | ||
|
|
||
| it('Blocks headers named by the Connection response header', () => { | ||
| const connectionHeader = 'keep-alive, x-private-hop, X-Another-Hop'; | ||
|
|
||
| expect(isAllowedProxyResponseHeader('x-private-hop', connectionHeader)).to.be.false; | ||
| expect(isAllowedProxyResponseHeader('x-another-hop', connectionHeader)).to.be.false; | ||
| expect(isAllowedProxyResponseHeader('content-type', connectionHeader)).to.be.true; | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.