Security controls bypass or absence In node-axios
Description
Axios: Incomplete Fix for CVE-2025-62718 — NO_PROXY Protection Bypassed via RFC 1122 Loopback Subnet (127.0.0.0/8) in Axios 1.15.0 1. Executive Summary This report documents an incomplete security patch for the previously disclosed vulnerability GHSA-3p68-rc4w-qgx5 (CVE-2025-62718), which affects the NO_PROXY hostname resolution logic in the Axios HTTP library. Background — The Original Vulnerability The original vulnerability (GHSA-3p68-rc4w-qgx5) disclosed that Axios did not normalize hostnames before comparing them against NO_PROXY rules. Specifically, a request to http://localhost./ (with a trailing dot) or http://[::1]/ (with IPv6 bracket notation) would bypass NO_PROXY matching entirely and be forwarded to the configured HTTP proxy — even when NO_PROXY=localhost,127.0.0.1,::1 was explicitly set by the developer to protect loopback services. The Axios maintainers addressed this in version 1.15.0 by introducing a normalizeNoProxyHost() function in lib/helpers/shouldBypassProxy.js, which strips trailing dots from hostnames and removes brackets from IPv6 literals before performing the NO_PROXY comparison. The Incomplete Patch — This Finding While the patch correctly addresses the specific cases reported (trailing dot normalization and IPv6 bracket removal), the fix is architecturally incomplete. The patch introduced a hardcoded set of recognized loopback addresses: // lib/helpers/shouldBypassProxy.js — Line 1 const LOOPBACK_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); However, RFC 1122 §3.2.1.3 explicitly defines the entire 127.0.0.0/8 subnet as the IPv4 loopback address block not just the single address 127.0.0.1. On all major operating systems (Linux, macOS, Windows with WSL), any IP address in the range 127.0.0.2 through 127.255.255.254 is a valid, functional loopback address that routes to the local machine. As a result, an attacker who can influence the target URL of an Axios request can substitute 127.0.0.1 with any other address in the 127.0.0.0/8 range (e.g., 127.0.0.2, 127.0.0.100, 127.1.2.3) to completely bypass the NO_PROXY protection even in the fully patched Axios 1.15.0 release. Verification This bypass has been independently verified on: * Axios version: 1.15.0 (latest patched release) * Node.js version: v22.16.0 * OS: Kali Linux (rolling) The Proof-of-Concept demonstrates that while localhost, localhost., and [::1] are correctly blocked by the patched version, requests to 127.0.0.2, 127.0.0.100, and 127.1.2.3 are transparently forwarded to the attacker-controlled proxy server, confirming that the patch does not cover the full RFC-defined loopback address space. 2. Deep-Dive: Technical Root Cause Analysis 2.1 Vulnerable File & Location | Field | Detail | | ------------- | ------------- | | File | lib/helpers/shouldBypassProxy.js| | Primary Flaw| isLoopback() — Line 1–3 | | Supporting Function | shouldBypassProxy() — Line 59–110 | | Axios Version | 1.15.0 (Latest Patched Release) | 2.2 How Axios Routes HTTP Requests The Call Chain When Axios dispatches any HTTP request, lib/adapters/http.js calls setProxy(), which invokes shouldBypassProxy() to decide whether to honour a configured proxy: // lib/adapters/http.js — Lines 191–199 function setProxy(options, configProxy, location) { let proxy = configProxy; if (!proxy && proxy !== false) { const proxyUrl = getProxyForUrl(location); // Step 1: Read proxy env var if (proxyUrl) { if (!shouldBypassProxy(location)) { // Step 2: Check NO_PROXY proxy = new URL(proxyUrl); // Step 3: Assign proxy } } } } shouldBypassProxy() is the single gatekeeper for NO_PROXY enforcement. A bypass here means all proxy protection fails silently. 2.3 The Original Vulnerability (GHSA-3p68-rc4w-qgx5) Before Axios 1.15.0, hostnames were compared against NO_PROXY using a raw literal string match with no normalization: Request URL → http://localhost./secret NO_PROXY → "localhost,127.0.0.1,::1" Comparison: "localhost." === "localhost" → FALSE → Proxy used ← BYPASS "[::1]" === "::1" → FALSE → Proxy used ← BYPASS Both localhost. (FQDN trailing dot, RFC 1034 §3.1) and [::1] (bracketed IPv6 literal, RFC 3986 §3.2.2) are canonical representations of loopback addresses, but Axios treated them as unknown hosts. 2.4 What the Patch Fixed (Axios 1.15.0) The patch introduced three changes inside lib/helpers/shouldBypassProxy.js: Fix A
normalizeNoProxyHost() (Lines 47–57) Strips alternate representations before comparison: const normalizeNoProxyHost = (hostname) => { if (!hostname) return hostname; // Remove IPv6 brackets: "[::1]" → "::1" if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') { hostname = hostname.slice(1, -1); } // Strip trailing FQDN dot: "localhost." → "localhost" return hostname.replace(/\.+$/, ''); }; Fix B Cross-Loopback Equivalence (Lines 1–3 & 108) Allows 127.0.0.1 and localhost to match each other interchangeably: const LOOPBACK_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); const isLoopback = (host) => LOOPBACK_ADDRESSES.has(host); // Line 108 — Final match condition: return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // If both sides are "loopback" → treat as match Fix C Normalization Applied on Both Sides (Lines 81 & 90) // Request hostname normalized: const hostname = normalizeNoProxyHost(parsed.hostname.toLowerCase()); // Each NO_PROXY entry normalized: entryHost = normalizeNoProxyHost(entryHost); 2.5 The Incomplete Patch Exact Root Cause The fundamental flaw resides in Line 1: // lib/helpers/shouldBypassProxy.js — Line 1 ← ROOT CAUSE const LOOPBACK_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); // ^^^^^^^^^^^ // Only ONE IPv4 loopback address is recognized. // The entire 127.0.0.0/8 subnet is unaccounted for. // Line 3 — Lookup against this incomplete set: const isLoopback = (host) => LOOPBACK_ADDRESSES.has(host); // ^^^^^^^^^ // Returns FALSE for any 127.x.x.x ≠ 127.0.0.1 *RFC 1122 §3.2.1.3 is unambiguous: > "The address 127.0.0.0/8 is assigned for loopback. A datagram sent by a higher-level protocol to a loopback address MUST NOT appear on any network." This means all addresses from
127.0.0.1 through 127.255.255.254 are valid loopback addresses on any RFC-compliant operating system. On Linux, the entire /8 block is routed to the lo interface by default. The patch recognises only 127.0.0.1, leaving 16,777,213 valid loopback addresses unprotected. 2.6 Step-by-Step Bypass Execution Trace Environment:
NO_PROXY = "localhost,127.0.0.1,::1" HTTP_PROXY = "http://attacker-proxy:5300" Target URL = "http://127.0.0.2:9191/internal-api" Annotated execution of shouldBypassProxy("http://127.0.0.2:9191/internal-api"): ``` // Step 1 — Parse the request URL parsed = new URL("http://127.0.0.2:9191/internal-api") hostname = "127.0.0.2" // parsed.hostname // Step 2 — Read NO_PROXY environment variable noProxy = "localhost,127.0.0.1,::1" // lowercased // Step 3 — Normalize the request hostname hostname = normalizeNoProxyHost("127.0.0.2") // No brackets → skip // No trailing dot → skip // Result: "127.0.0.2" (unchanged) // Step 4 —
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
debian 14 | 1.15.2-1 | ||
debian 11 | - | ||
debian 12 | - | ||
debian 13 | - | ||
rpm rhel8 | - | - | |
npm | 1.15.1, 0.31.1 |
Aliases
References