From e033f243a08e3514c03e510f76658da1e0fac3bd Mon Sep 17 00:00:00 2001 From: Jay Date: Sat, 18 Apr 2026 17:41:15 +0200 Subject: [PATCH] fix: incomplete fix for cve (#10755) --- lib/helpers/shouldBypassProxy.js | 47 +++++++++++++- tests/unit/helpers/shouldBypassProxy.test.js | 66 ++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/lib/helpers/shouldBypassProxy.js b/lib/helpers/shouldBypassProxy.js index d90902c4..95a85e1f 100644 --- a/lib/helpers/shouldBypassProxy.js +++ b/lib/helpers/shouldBypassProxy.js @@ -1,6 +1,49 @@ -const LOOPBACK_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); +const LOOPBACK_HOSTNAMES = new Set(['localhost']); -const isLoopback = (host) => LOOPBACK_ADDRESSES.has(host); +const isIPv4Loopback = (host) => { + const parts = host.split('.'); + if (parts.length !== 4) return false; + if (parts[0] !== '127') return false; + return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255); +}; + +const isIPv6Loopback = (host) => { + // Collapse all-zero groups: any form of ::1 / 0:0:...:0:1 + // First, strip any leading "::" by normalising with Set lookup of common forms, + // then fall back to structural check. + if (host === '::1') return true; + + // Check IPv4-mapped IPv6 loopback: ::ffff: or ::ffff: + // Node's URL parser normalises ::ffff:127.0.0.1 → ::ffff:7f00:1 + const v4MappedDotted = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i); + if (v4MappedDotted) return isIPv4Loopback(v4MappedDotted[1]); + + const v4MappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i); + if (v4MappedHex) { + const high = parseInt(v4MappedHex[1], 16); + // High 16 bits must start with 127 (0x7f) — i.e. 0x7f00..0x7fff + return high >= 0x7f00 && high <= 0x7fff; + } + + // Full-form ::1 variants: any number of zero groups followed by trailing 1 + // e.g. 0:0:0:0:0:0:0:1, 0000:...:0001 + const groups = host.split(':'); + if (groups.length === 8) { + for (let i = 0; i < 7; i++) { + if (!/^0+$/.test(groups[i])) return false; + } + return /^0*1$/.test(groups[7]); + } + + return false; +}; + +const isLoopback = (host) => { + if (!host) return false; + if (LOOPBACK_HOSTNAMES.has(host)) return true; + if (isIPv4Loopback(host)) return true; + return isIPv6Loopback(host); +}; const DEFAULT_PORTS = { http: 80, diff --git a/tests/unit/helpers/shouldBypassProxy.test.js b/tests/unit/helpers/shouldBypassProxy.test.js index 4a3deacb..a05fffd4 100644 --- a/tests/unit/helpers/shouldBypassProxy.test.js +++ b/tests/unit/helpers/shouldBypassProxy.test.js @@ -100,4 +100,70 @@ describe('helpers::shouldBypassProxy', () => { expect(shouldBypassProxy('not a url')).toBe(false); }); + + it('should bypass proxy for 127.0.0.0/8 subnet when no_proxy contains 127.0.0.1 (GHSA-pmwg-cvhr-8vh7)', () => { + setNoProxy('localhost,127.0.0.1,::1'); + + expect(shouldBypassProxy('http://127.0.0.2:9191/secret')).toBe(true); + expect(shouldBypassProxy('http://127.0.0.100:9191/secret')).toBe(true); + expect(shouldBypassProxy('http://127.1.2.3:9191/secret')).toBe(true); + expect(shouldBypassProxy('http://127.255.255.254:9191/secret')).toBe(true); + }); + + it('should bypass proxy for 127.0.0.0/8 subnet when no_proxy contains localhost', () => { + setNoProxy('localhost'); + + expect(shouldBypassProxy('http://127.0.0.2:7777/')).toBe(true); + expect(shouldBypassProxy('http://127.1.2.3:7777/')).toBe(true); + }); + + it('should NOT bypass for non-loopback IPv4 addresses', () => { + setNoProxy('localhost,127.0.0.1,::1'); + + expect(shouldBypassProxy('http://128.0.0.1:9191/')).toBe(false); + expect(shouldBypassProxy('http://126.255.255.255:9191/')).toBe(false); + expect(shouldBypassProxy('http://10.0.0.1:9191/')).toBe(false); + expect(shouldBypassProxy('http://192.168.1.1:9191/')).toBe(false); + }); + + it('should NOT treat malformed 127-prefixed values as loopback', () => { + setNoProxy('localhost,127.0.0.1,::1'); + + // bracketed IPv6 that happens to contain 127 dotted-form must not match IPv4 loopback + expect(shouldBypassProxy('http://example.com/')).toBe(false); + }); + + it('should bypass proxy for full-form IPv6 loopback 0:0:0:0:0:0:0:1', () => { + setNoProxy('localhost,127.0.0.1,::1'); + + expect(shouldBypassProxy('http://[0:0:0:0:0:0:0:1]:8080/')).toBe(true); + }); + + it('should bypass proxy for IPv4-mapped IPv6 loopback ::ffff:127.0.0.1', () => { + setNoProxy('localhost,127.0.0.1,::1'); + + expect(shouldBypassProxy('http://[::ffff:127.0.0.1]:8080/')).toBe(true); + }); + + it('should treat 127.x.x.x as cross-equivalent to localhost and ::1', () => { + setNoProxy('::1'); + + expect(shouldBypassProxy('http://127.0.0.5:7777/')).toBe(true); + }); + + it('should still respect explicit port mismatch on no_proxy entries', () => { + setNoProxy('127.0.0.1:8080'); + + // same-port → bypass via cross-loopback equivalence + expect(shouldBypassProxy('http://127.0.0.2:8080/')).toBe(true); + // different port → no bypass + expect(shouldBypassProxy('http://127.0.0.2:9090/')).toBe(false); + }); + + it('should not bypass for hosts that merely contain 127 in other octets', () => { + setNoProxy('localhost,127.0.0.1,::1'); + + expect(shouldBypassProxy('http://10.0.0.127:8080/')).toBe(false); + expect(shouldBypassProxy('http://200.127.0.1:8080/')).toBe(false); + }); });