@hapi/wreck: Sensitive credential headers leak across cross-port and cross-scheme redirects
Description
Wreck HTTP client leaks credential headers across cross-origin redirects that differ only in port or scheme, allowing credential theft; fixed in 18.1.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Wreck HTTP client leaks credential headers across cross-origin redirects that differ only in port or scheme, allowing credential theft; fixed in 18.1.2.
Vulnerability
In Wreck (HTTP client library for hapi.js), versions prior to 18.1.2, the cross-origin redirect check only compares hostnames, ignoring scheme and port. This causes credential headers (Authorization, Cookie, Proxy-Authorization) to be forwarded when redirecting to a different port on the same host or from HTTPS to HTTP, violating the same-origin policy.
Exploitation
An attacker who can control a redirect response (e.g., via a co-tenant on an adjacent port, or a network position that can forge HTTP redirects) can cause the client to send sensitive credentials to a different origin. No user interaction beyond following the redirect is required; the client automatically follows redirects if redirects is set to a positive number (default is 0, but users may enable it).
Impact
Successful exploitation allows the attacker to capture bearer tokens, session cookies, and proxy credentials, enabling impersonation of the victim against the upstream service. This leads to unauthorized access and potential data breach.
Mitigation
Upgrade to Wreck version 18.1.2 or later, which performs a full origin comparison (scheme, host, port). Workarounds include setting redirects: 0 (the default) and handling redirects manually with a strict origin check, or using the beforeRedirect hook to inspect the redirect target and strip sensitive headers before the follow-on request. [1][2]
AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
1b93323b63ad3fix: cross-origin redirect should also remove sensitive headers
2 files changed · +90 −4
lib/index.js+2 −2 modified@@ -249,10 +249,10 @@ internals.Client = class { redirectOptions.timeout = (redirectOptions.timeout - elapsed).toString(); // stringify to not drop timeout when === 0 } - // When redirecting to a new hostname, remove sensitive credential headers + // When redirecting cross-origin (scheme, host, or port differs), remove sensitive credential headers if (redirectOptions.headers) { const parsedLocation = new URL(location); - if (uri.hostname !== parsedLocation.hostname) { + if (uri.origin !== parsedLocation.origin) { for (const header of Object.keys(redirectOptions.headers)) { if (internals.sensitiveCrossHostHeaders.has(header.toLowerCase())) { delete redirectOptions.headers[header];
test/index.js+88 −2 modified@@ -442,6 +442,75 @@ describe('request()', () => { http2.close(); }); + it('removes sensitive headers when redirected to a different port on the same hostname', async (flags) => { + + const handler2 = (req, res) => { + + if (req.headers.authorization || req.headers.cookie || req.headers['proxy-authorization']) { + res.writeHead(500); + return res.end(); + } + + res.writeHead(200); + res.end(); + }; + + const http2 = await internals.server(handler2); + + const handler1 = (req, res) => { + + res.writeHead(302, { 'Location': 'http://localhost:' + http2.address().port }); + res.end(); + }; + + const http1 = await internals.server(handler1); + + const headers = { + authorization: 'some-auth-key', + cookie: 'some-cookie', + 'proxy-authorization': 'some-proxy-auth' + }; + + const res = await Wreck.request('get', 'http://localhost:' + http1.address().port, { redirects: 1, headers }); + expect(res.statusCode).to.equal(200); + http1.close(); + http2.close(); + }); + + it('removes sensitive headers when redirected to a different scheme on the same hostname', async (flags) => { + + const handler1 = (req, res) => { + + res.writeHead(302, { 'Location': 'https://127.0.0.1:' + https.address().port }); + res.end(); + }; + + const handler2 = (req, res) => { + + if (req.headers.authorization || req.headers.cookie || req.headers['proxy-authorization']) { + res.writeHead(500); + return res.end(); + } + + res.writeHead(200); + res.end(); + }; + + const https = await internals.https(handler2); + const http = await internals.server(handler1); + + const headers = { + authorization: 'some-auth-key', + cookie: 'some-cookie', + 'proxy-authorization': 'some-proxy-auth' + }; + + const res = await Wreck.request('get', 'http://localhost:' + http.address().port, { redirects: 1, rejectUnauthorized: false, headers }); + expect(res.statusCode).to.equal(200); + http.close(); + https.close(); + }); + it('preserves proxy-authorization header on same-hostname redirect', async (flags) => { let gen = 0; @@ -1303,9 +1372,10 @@ describe('options.baseUrl', () => { it('uses path when path is a full URL', async (flags) => { - const promise = Wreck.request('get', 'http://localhost:8080/foo', { baseUrl: 'http://localhost:0/' }); + const unboundPort = await internals.unusedPort(); + const promise = Wreck.request('get', `http://localhost:${unboundPort}/foo`, { baseUrl: 'http://localhost:0/' }); await expect(promise).to.reject(); - expect(promise.req.getHeader('host')).to.equal('localhost:8080'); + expect(promise.req.getHeader('host')).to.equal(`localhost:${unboundPort}`); }); it('uses lower-case host header when path is not a full URL', async (flags) => { @@ -2601,6 +2671,22 @@ describe('Defaults', () => { }); +internals.unusedPort = function () { + + return new Promise((resolve, reject) => { + + const server = Http.createServer(); + server.unref(); + server.on('error', reject); + server.listen(0, '127.0.0.1', () => { + + const { port } = server.address(); + server.close(() => resolve(port)); + }); + }); +}; + + internals.server = function (handler, socket) { if (typeof handler !== 'function') {
Vulnerability mechanics
Root cause
"The origin check in the redirect handler compared only hostnames, ignoring scheme and port, so credentials were forwarded to cross-origin targets that shared the same hostname."
Attack vector
An attacker who can control a redirect response (e.g., through a malicious upstream, a man-in-the-middle position, or a co-tenant on an adjacent port) can craft a `Location` header that differs only in scheme or port while keeping the same hostname. Because the old code compared only hostnames, Wreck would forward sensitive headers (`Authorization`, `Cookie`, `Proxy-Authorization`) to the cross-origin target. This allows credential theft and session hijacking.
Affected code
The vulnerability is in `lib/index.js` in the redirect-handling logic of the `internals.Client` class. The origin check at line 252 used `uri.hostname !== parsedLocation.hostname`, which compared only hostnames and ignored scheme and port. The patch changes this to `uri.origin !== parsedLocation.origin` to perform a full origin comparison.
What the fix does
The patch replaces the hostname-only comparison (`uri.hostname !== parsedLocation.hostname`) with a full-origin comparison (`uri.origin !== parsedLocation.origin`) in `lib/index.js`. This ensures that sensitive credential headers are stripped whenever the scheme, host, or port differs between the original request and the redirect target, aligning with the WHATWG Fetch same-origin definition.
Preconditions
- networkThe attacker must be able to influence the redirect target (e.g., via a malicious upstream service, a network position that can inject redirect headers, or a co-tenant on a different port of the same host).
- configThe Wreck client must be configured with `redirects > 0` (default is 0, but any positive value enables automatic redirect following).
- inputThe initial request must include at least one of the sensitive headers: `Authorization`, `Cookie`, or `Proxy-Authorization`.
Generated on Jun 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.