axios < 1.7.4 does not correctly honor the NO_PROXY environment variable, allowing internal network access via hostnames that should be excluded from proxying.
axios.get(url) — the attacker bypasses NO_PROXY by controlling the hostname formatNO_PROXY environment variable. Hostnames with unusual formats (e.g. containing dots in unexpected positions, or using IPv4/IPv6 representations that should be excluded) were not matched against the NO_PROXY list, causing axios to route the request through the proxy when it should have been direct.NO_PROXY=169.254.169.254,localhost is set but axios doesn't recognize the metadata IP as NO_PROXY-exempt.Node.js and many HTTP clients use the NO_PROXY environment variable to specify hostnames that should bypass the system proxy. This is a critical security mechanism — for example, setting NO_PROXY=169.254.169.254 prevents tools from leaking cloud metadata credentials through the proxy when making requests to the metadata endpoint.
axios < 1.7.4's proxy-checks implementation incorrectly parsed the NO_PROXY variable. The parsing logic failed to handle certain hostname formats — particularly IPv4 addresses expressed in non-standard ways, and hostnames that contained characters that should trigger NO_PROXY matching but didn't due to a regex or string-comparison bug.
An attacker identifies a proxy route handler (e.g. GET /proxy?url=https://target) in the application. The handler passes the url query parameter to axios.get(). By crafting the target URL with a hostname that bypasses axios's NO_PROXY check, the attacker routes the request through the system proxy — which logs or forwards the request — rather than making a direct connection. The attacker exfiltrates data from internal services.
Real-world impact: When a service sets NO_PROXY=169.254.169.254,localhost,.internal expecting to prevent metadata and internal network access, axios < 1.7.4 still routes requests to those targets through the proxy (exposing credentials and data to proxy logs) or in some configurations through a direct connection that should have been blocked. Cloud workloads with IMDSv1 enabled are most at risk.
The fix replaces the ad-hoc NO_PROXY string parsing with a standards-compliant hostname matching function that correctly handles domain wildcards, IP addresses (including IPv4-mapped IPv6), port specifications, and unusual hostname encodings. This aligns axios's behavior with other HTTP clients (curl, wget, urllib) that handle NO_PROXY correctly.
*.example.com as a wildcard suffix, some treat example.com:8080 as port-specific, some ignore IPv4 addresses in certain formats. axios's proxy implementation was written before the WHATWG URL Standard's proxy bypass rules were widely understood, so the hostname matching was implemented as a simple string contains check rather than a proper URL parsing + pattern match. The fix aligns axios's behavior with curl and urllib's implementation of NO_PROXY.
User-controlled
urlquery parameter is passed directly toaxios.get(url)without hostname validation. Any user can request arbitrary URLs through this proxy — includinghttps://169.254.169.254/latest/meta-data/(AWS/GCP metadata),http://localhost:5432(internal database ports), or internal service URLs. Additionally, axios < 1.7.4 does not correctly honorNO_PROXY, so even environment-level proxy exclusions cannot be trusted as a mitigation. This is CWE-918 (SSRF). Upgrade axios to ≥ 1.7.4. As defense-in-depth, add a hostname allowlist and validate the resolved IP against a blocklist before making the request.```suggestion // Fix Option 1: Upgrade axios >= 1.7.4 // Fix Option 2: Add hostname allowlist AND IP validation as defense-in-depth: const ALLOWED_HOSTS = new Set([ 'api.example.com', 'status.example.com', 'api.github.com', ]); function isAllowedHost(url) { try { const { hostname } = new URL(url); return ALLOWED_HOSTS.has(hostname); } catch { return false; } } if (!isAllowedHost(url)) return res.status(403).send('Blocked'); const resp = await axios.get(url, { timeout: 3000 }); // Defense-in-depth: also check the resolved IP const ip = await dns.lookup(urlHostname); if (RESERVED_IP_RANGES.has(ip)) return res.status(403).send('Reserved IP'); ```