diff --git a/README.md b/README.md index 395cbdd8..a1d15c0d 100644 --- a/README.md +++ b/README.md @@ -928,6 +928,12 @@ These are the available config options for making requests. Only the `url` is re // This will set a `Proxy-Authorization` header, overwriting any existing // `Proxy-Authorization` custom headers you have set using `headers`. // If the proxy server uses HTTPS, then you must set the protocol to `https`. + // A user-supplied `Host` header in `headers` is preserved when forwarding + // through a proxy (case-insensitive match on `host`/`Host`/`HOST`); this + // lets you target a virtual host that differs from the request URL — for + // example, hitting `127.0.0.1:4000` while having the proxy treat the + // request as `example.com`. If no `Host` header is supplied, axios + // defaults it to the request URL's `hostname:port` as before. proxy: { protocol: 'https', host: '127.0.0.1', diff --git a/lib/adapters/http.js b/lib/adapters/http.js index 6337bf33..404dd18e 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -236,7 +236,18 @@ function setProxy(options, configProxy, location, isRedirect) { options.headers['Proxy-Authorization'] = 'Basic ' + base64; } - options.headers.host = options.hostname + (options.port ? ':' + options.port : ''); + // Preserve a user-supplied Host header (case-insensitive) so callers can override + // the value forwarded to the proxy; otherwise default to the request URL's host. + let hasUserHostHeader = false; + for (const name in options.headers) { + if (name.toLowerCase() === 'host') { + hasUserHostHeader = true; + break; + } + } + if (!hasUserHostHeader) { + options.headers.host = options.hostname + (options.port ? ':' + options.port : ''); + } const proxyHost = proxy.hostname || proxy.host; options.hostname = proxyHost; // Replace 'host' since options is not a URL object diff --git a/tests/unit/adapters/http.test.js b/tests/unit/adapters/http.test.js index 7d83281d..70c23bf1 100644 --- a/tests/unit/adapters/http.test.js +++ b/tests/unit/adapters/http.test.js @@ -2371,6 +2371,64 @@ describe('supports http with nodejs', () => { } }); + describe('Host header preservation when forwarding through a proxy (#10805)', () => { + const proxyConfig = { hostname: '127.0.0.1', protocol: 'http:', port: 8888 }; + + it('defaults the Host header to the request target when the user does not set one', () => { + const options = { + headers: {}, + beforeRedirects: {}, + hostname: '127.0.0.1', + port: 4000, + }; + + __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/'); + + assert.strictEqual(options.headers.host, '127.0.0.1:4000'); + }); + + it('preserves a user-supplied lowercase host header', () => { + const options = { + headers: { host: 'example.com' }, + beforeRedirects: {}, + hostname: '127.0.0.1', + port: 4000, + }; + + __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/'); + + assert.strictEqual(options.headers.host, 'example.com'); + }); + + it('preserves a user-supplied Host header regardless of casing', () => { + const options = { + headers: { Host: 'example.com' }, + beforeRedirects: {}, + hostname: '127.0.0.1', + port: 4000, + }; + + __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/'); + + assert.strictEqual(options.headers.Host, 'example.com'); + assert.strictEqual(options.headers.host, undefined); + }); + + it('preserves a user-supplied Host header across a redirect re-invocation', () => { + const options = { + headers: { Host: 'example.com' }, + beforeRedirects: {}, + hostname: '127.0.0.1', + port: 4000, + }; + + __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/', true); + + assert.strictEqual(options.headers.Host, 'example.com'); + assert.strictEqual(options.headers.host, undefined); + }); + }); + describe('Proxy-Authorization header leak on redirect (GHSA-j5f8-grm9-p9fc)', () => { it('clears a stale Proxy-Authorization header when redirected request resolves to no proxy (configProxy=false)', () => { const options = {