From 01e869dd28cefc52b144c8234e40cf3d56fcb182 Mon Sep 17 00:00:00 2001 From: Devendra Reddy Pennabadi Date: Tue, 16 Jun 2026 23:53:12 +0530 Subject: [PATCH] fix(http): accept path-only url when socketPath is set (#6611) (#10930) Co-authored-by: Jason Saayman --- PRE_RELEASE_CHANGELOG.md | 6 +++- lib/adapters/http.js | 11 ++++-- tests/unit/adapters/http.test.js | 58 +++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/PRE_RELEASE_CHANGELOG.md b/PRE_RELEASE_CHANGELOG.md index 20034926..ce598c2f 100644 --- a/PRE_RELEASE_CHANGELOG.md +++ b/PRE_RELEASE_CHANGELOG.md @@ -4,4 +4,8 @@ ## Bug Fixes -- **Proxy Agent Streams:** Guarded Node HTTP adapter TCP keep-alive setup so proxy agents that return generic Duplex streams do not throw when `setKeepAlive` is unavailable. (**#10917**, closes **#10908**) \ No newline at end of file +- **HTTP Adapter - socketPath:** Path-only request URLs (e.g. `'/foo'`) now work again with `config.socketPath`, fixing the `TypeError [ERR_INVALID_URL]` regression introduced in 1.7.4 when `new URL()` was added to the dispatch path. A synthetic `http://localhost` base is supplied only when an own `socketPath` is set, so absolute URLs, non-socket requests, and prototype-polluted `socketPath` values are unaffected. (**#6611**) + +## Release Tracking + +- **Proxy Agent Streams:** Guarded Node HTTP adapter TCP keep-alive setup so proxy agents that return generic Duplex streams do not throw when `setKeepAlive` is unavailable. (**#10917**, closes **#10908**) diff --git a/lib/adapters/http.js b/lib/adapters/http.js index c9a6b609..bd5e7f86 100755 --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -477,6 +477,7 @@ export default isHttpAdapterSupported && let http2Options = own('http2Options'); const responseType = own('responseType'); const responseEncoding = own('responseEncoding'); + const socketPath = own('socketPath'); const httpAgent = own('httpAgent'); const httpsAgent = own('httpsAgent'); const method = own('method').toUpperCase(); @@ -603,7 +604,14 @@ export default isHttpAdapterSupported && // Parse url const fullPath = buildFullPath(own('baseURL'), own('url'), own('allowAbsoluteUrls'), config); - const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined); + // Unix-socket requests (own socketPath) commonly pass a path-only url + // like '/foo'; supply a synthetic base so new URL() can still parse it. + // Use the own-property value (not config.socketPath) so a polluted + // prototype cannot influence URL base selection. + const urlBase = socketPath + ? 'http://localhost' + : (platform.hasBrowserEnv ? platform.origin : undefined); + const parsed = new URL(fullPath, urlBase); const protocol = parsed.protocol || supportedProtocols[0]; if (protocol === 'data:') { @@ -842,7 +850,6 @@ export default isHttpAdapterSupported && // cacheable-lookup integration hotfix !utils.isUndefined(lookup) && (options.lookup = lookup); - const socketPath = own('socketPath'); if (socketPath) { if (typeof socketPath !== 'string') { return reject( diff --git a/tests/unit/adapters/http.test.js b/tests/unit/adapters/http.test.js index 959ace72..b9bc35a0 100644 --- a/tests/unit/adapters/http.test.js +++ b/tests/unit/adapters/http.test.js @@ -6368,9 +6368,10 @@ describe('supports http with nodejs', () => { : path.join(os.tmpdir(), `${pipe}.sock`); } - function startUnixServer(socketPath) { + function startUnixServer(socketPath, onRequest) { return new Promise((resolveStart, rejectStart) => { const server = http.createServer((req, res) => { + onRequest && onRequest(req); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, url: req.url })); }); @@ -6409,6 +6410,61 @@ describe('supports http with nodejs', () => { } }); + it('accepts a path-only url when socketPath is set (regression #6611)', async () => { + const socketPath = makeSocketPath(); + const server = await startUnixServer(socketPath); + try { + const res = await axios.get('/echo?q=1', { socketPath }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.ok, true); + assert.strictEqual(res.data.url, '/echo?q=1'); + } finally { + await stopUnixServer(server, socketPath); + } + }); + + it('accepts a path-only url when socketPath matches allowedSocketPaths', async () => { + const socketPath = makeSocketPath(); + const server = await startUnixServer(socketPath); + try { + const res = await axios.get('/echo?q=1', { + socketPath, + allowedSocketPaths: [socketPath], + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.ok, true); + assert.strictEqual(res.data.url, '/echo?q=1'); + } finally { + await stopUnixServer(server, socketPath); + } + }); + + it('ignores a prototype-polluted socketPath (security, regression #6611)', async () => { + const socketPath = makeSocketPath(); + let requestCount = 0; + const server = await startUnixServer(socketPath, () => { + requestCount += 1; + }); + // Pollute the prototype so `socketPath` is visible via the chain but is + // NOT an own property of the request config. + Object.prototype.socketPath = socketPath; + try { + // With no own socketPath, the polluted prototype value must not be + // honored: the path-only url gets no synthetic base and the request is + // never routed to the (attacker-controlled) socket, so it rejects + // instead of silently connecting. + await assert.rejects(axios.get('/echo?q=1'), (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.code, AxiosError.ERR_INVALID_URL); + return true; + }); + assert.strictEqual(requestCount, 0); + } finally { + delete Object.prototype.socketPath; + await stopUnixServer(server, socketPath); + } + }); + it('allows socketPath when it matches an allowedSocketPaths string', async () => { const socketPath = makeSocketPath(); const server = await startUnixServer(socketPath);