Allocation of Resources Without Limits or Throttling in Axios
Description
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', ...]whenfetchis selected- environments where neither
xhrnorhttpis available and axios falls back tofetch - custom fetch environments configured through
env.fetch
Unaffected functionality includes:
- Node.js default
httpadapter 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:
maxContentLengthandmaxBodyLengthreads inlib/adapters/fetch.js- upfront
data:URL decoded-size checks - outbound body-size checks before dispatch
Content-Lengthresponse 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
import http from 'node:http';
import axios from 'axios';
const server = http.createServer((req, res) => {
let received = 0;
req.on('data', chunk => {
received += chunk.length;
});
req.on('end', () => {
res.end(JSON.stringify({ received }));
});
});
await new Promise(resolve => server.listen(0, resolve));
const url = `http://127.0.0.1:${server.address().port}/`;
await axios.post(url, 'A'.repeat(2 * 1024 * 1024), {
adapter: 'fetch',
maxBodyLength: 1024
});
// Vulnerable versions succeed and the server receives 2097152 bytes.
// Fixed versions reject with ERR_BAD_REQUEST.
server.close();
Workarounds
Use the Node.js http adapter for server-side requests where finite size limits are security-relevant.
Validate or cap attacker-controlled request bodies before passing them to axios.
Reject or strictly allowlist attacker-controlled URL schemes, especially data: URLs, before calling axios.
Original Report
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.
---
Affected products
2Patches
2d8165e9f2c4afix: incorrect assumption on test (#10796)
2 files changed · +24 −7
lib/adapters/fetch.js+1 −0 modified@@ -312,6 +312,7 @@ const factory = (env) => { if ( supportsResponseStream && + response.body && (onDownloadProgress || hasMaxContentLength || (isStreamResponse && unsubscribe)) ) { const options = {};
lib/helpers/estimateDataURLDecodedBytes.js+23 −7 modified@@ -73,12 +73,28 @@ export default function estimateDataURLDecodedBytes(url) { return Buffer.byteLength(body, 'utf8'); } - // Browser/worker fallback: use TextEncoder when available, else fall back to - // raw string length as an upper-bound heuristic. Both are safe for a DoS - // guard (over-counting only makes the check stricter for non-ASCII content). - if (typeof TextEncoder === 'function') { - return new TextEncoder().encode(body).byteLength; + // Compute UTF-8 byte length directly from UTF-16 code units without allocating + // a byte buffer (TextEncoder.encode would defeat the DoS guard on large bodies). + // Using body.length here would undercount non-ASCII (e.g. '€' is 1 code unit + // but 3 UTF-8 bytes). + let bytes = 0; + for (let i = 0, len = body.length; i < len; i++) { + const c = body.charCodeAt(i); + if (c < 0x80) { + bytes += 1; + } else if (c < 0x800) { + bytes += 2; + } else if (c >= 0xd800 && c <= 0xdbff && i + 1 < len) { + const next = body.charCodeAt(i + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + bytes += 4; + i++; + } else { + bytes += 3; + } + } else { + bytes += 3; + } } - - return body.length; + return bytes; }
e5540dcafe42fix: fetch adaptor is not enforcing max body or content length (#10795)
3 files changed · +280 −3
lib/adapters/fetch.js+103 −2 modified@@ -11,6 +11,7 @@ import { } from '../helpers/progressEventReducer.js'; import resolveConfig from '../helpers/resolveConfig.js'; import settle from '../core/settle.js'; +import estimateDataURLDecodedBytes from '../helpers/estimateDataURLDecodedBytes.js'; const DEFAULT_CHUNK_SIZE = 64 * 1024; @@ -163,8 +164,13 @@ const factory = (env) => { headers, withCredentials = 'same-origin', fetchOptions, + maxContentLength, + maxBodyLength, } = resolveConfig(config); + const hasMaxContentLength = utils.isNumber(maxContentLength) && maxContentLength > -1; + const hasMaxBodyLength = utils.isNumber(maxBodyLength) && maxBodyLength > -1; + let _fetch = envFetch || fetch; responseType = responseType ? (responseType + '').toLowerCase() : 'text'; @@ -186,6 +192,41 @@ const factory = (env) => { let requestContentLength; try { + // Enforce maxContentLength for data: URLs up-front so we never materialize + // an oversized payload. The HTTP adapter applies the same check (see http.js + // "if (protocol === 'data:')" branch). + if (hasMaxContentLength && typeof url === 'string' && url.startsWith('data:')) { + const estimated = estimateDataURLDecodedBytes(url); + if (estimated > maxContentLength) { + throw new AxiosError( + 'maxContentLength size of ' + maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, + config, + request + ); + } + } + + // Enforce maxBodyLength against the outbound request body before dispatch. + // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than + // maxBodyLength limit'). Skip when the body length cannot be determined + // (e.g. a live ReadableStream supplied by the caller). + if (hasMaxBodyLength && method !== 'get' && method !== 'head') { + const outboundLength = await resolveBodyLength(headers, data); + if ( + typeof outboundLength === 'number' && + isFinite(outboundLength) && + outboundLength > maxBodyLength + ) { + throw new AxiosError( + 'Request body larger than maxBodyLength limit', + AxiosError.ERR_BAD_REQUEST, + config, + request + ); + } + } + if ( onUploadProgress && supportsRequestStream && @@ -252,10 +293,27 @@ const factory = (env) => { ? _fetch(request, fetchOptions) : _fetch(url, resolvedOptions)); + // Cheap pre-check: if the server honestly declares a content-length that + // already exceeds the cap, reject before we start streaming. + if (hasMaxContentLength) { + const declaredLength = utils.toFiniteNumber(response.headers.get('content-length')); + if (declaredLength != null && declaredLength > maxContentLength) { + throw new AxiosError( + 'maxContentLength size of ' + maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, + config, + request + ); + } + } + const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response'); - if (supportsResponseStream && (onDownloadProgress || (isStreamResponse && unsubscribe))) { + if ( + supportsResponseStream && + (onDownloadProgress || hasMaxContentLength || (isStreamResponse && unsubscribe)) + ) { const options = {}; ['status', 'statusText', 'headers'].forEach((prop) => { @@ -272,8 +330,24 @@ const factory = (env) => { )) || []; + let bytesRead = 0; + const onChunkProgress = (loadedBytes) => { + if (hasMaxContentLength) { + bytesRead = loadedBytes; + if (bytesRead > maxContentLength) { + throw new AxiosError( + 'maxContentLength size of ' + maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, + config, + request + ); + } + } + onProgress && onProgress(loadedBytes); + }; + response = new Response( - trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => { + trackStream(response.body, DEFAULT_CHUNK_SIZE, onChunkProgress, () => { flush && flush(); unsubscribe && unsubscribe(); }), @@ -288,6 +362,33 @@ const factory = (env) => { config ); + // Fallback enforcement for environments without ReadableStream support + // (legacy runtimes). Detect materialized size from typed output; skip + // streams/Response passthrough since the user will read those themselves. + if (hasMaxContentLength && !supportsResponseStream && !isStreamResponse) { + let materializedSize; + if (responseData != null) { + if (typeof responseData.byteLength === 'number') { + materializedSize = responseData.byteLength; + } else if (typeof responseData.size === 'number') { + materializedSize = responseData.size; + } else if (typeof responseData === 'string') { + materializedSize = + typeof TextEncoder === 'function' + ? new TextEncoder().encode(responseData).byteLength + : responseData.length; + } + } + if (typeof materializedSize === 'number' && materializedSize > maxContentLength) { + throw new AxiosError( + 'maxContentLength size of ' + maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, + config, + request + ); + } + } + !isStreamResponse && unsubscribe && unsubscribe(); return await new Promise((resolve, reject) => {
lib/helpers/estimateDataURLDecodedBytes.js+12 −1 modified@@ -69,5 +69,16 @@ export default function estimateDataURLDecodedBytes(url) { return bytes > 0 ? bytes : 0; } - return Buffer.byteLength(body, 'utf8'); + if (typeof Buffer !== 'undefined' && typeof Buffer.byteLength === 'function') { + return Buffer.byteLength(body, 'utf8'); + } + + // Browser/worker fallback: use TextEncoder when available, else fall back to + // raw string length as an upper-bound heuristic. Both are safe for a DoS + // guard (over-counting only makes the check stricter for non-ASCII content). + if (typeof TextEncoder === 'function') { + return new TextEncoder().encode(body).byteLength; + } + + return body.length; }
tests/unit/adapters/fetch.test.js+165 −0 modified@@ -732,6 +732,171 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => }); }); + describe('size limits (GHSA-777c-7fjr-54vf)', () => { + it('should reject an outbound body that exceeds maxBodyLength with ERR_BAD_REQUEST', async () => { + const server = await startHTTPServer( + (req, res) => { + res.end('ok'); + }, + { port: SERVER_PORT } + ); + + try { + await assert.rejects( + fetchAxios.post(`${LOCAL_SERVER_URL}/`, 'A'.repeat(2048), { + maxBodyLength: 1024, + }), + (err) => { + assert.strictEqual(err.code, 'ERR_BAD_REQUEST'); + assert.match(err.message, /Request body larger than maxBodyLength limit/); + return true; + } + ); + } finally { + await stopHTTPServer(server); + } + }); + + it('should reject a response whose Content-Length exceeds maxContentLength with ERR_BAD_RESPONSE', async () => { + const payload = 'A'.repeat(8 * 1024); + const server = await startHTTPServer( + (req, res) => { + res.setHeader('Content-Length', Buffer.byteLength(payload)); + res.end(payload); + }, + { port: SERVER_PORT } + ); + + try { + await assert.rejects( + fetchAxios.get(`${LOCAL_SERVER_URL}/`, { + maxContentLength: 1024, + }), + (err) => { + assert.strictEqual(err.code, 'ERR_BAD_RESPONSE'); + assert.match(err.message, /maxContentLength size of 1024 exceeded/); + return true; + } + ); + } finally { + await stopHTTPServer(server); + } + }); + + it('should reject a chunked response that exceeds maxContentLength during streaming', async () => { + const server = await startHTTPServer( + (req, res) => { + // Omit content-length so the cheap pre-check cannot fire; force + // the stream-based enforcement path. + res.setHeader('Transfer-Encoding', 'chunked'); + const chunk = 'B'.repeat(1024); + let sent = 0; + const writeNext = () => { + if (sent >= 8) { + return res.end(); + } + sent++; + res.write(chunk, writeNext); + }; + writeNext(); + }, + { port: SERVER_PORT } + ); + + try { + await assert.rejects( + fetchAxios.get(`${LOCAL_SERVER_URL}/`, { + maxContentLength: 512, + }), + (err) => { + assert.strictEqual(err.code, 'ERR_BAD_RESPONSE'); + assert.match(err.message, /maxContentLength size of 512 exceeded/); + return true; + } + ); + } finally { + await stopHTTPServer(server); + } + }); + + it('should reject a data: URL whose decoded size exceeds maxContentLength (base64)', async () => { + const payload = 'A'.repeat(4096); + const dataUrl = 'data:application/octet-stream;base64,' + Buffer.from(payload).toString('base64'); + + // Use a dedicated instance without baseURL — combineURLs would otherwise + // prepend baseURL to a data: URL and neutralise the pre-check. + const bareAxios = axios.create({ adapter: 'fetch' }); + + await assert.rejects( + bareAxios.get(dataUrl, { maxContentLength: 16 }), + (err) => { + assert.strictEqual(err.code, 'ERR_BAD_RESPONSE'); + assert.match(err.message, /maxContentLength size of 16 exceeded/); + return true; + } + ); + }); + + it('should reject a data: URL whose body size exceeds maxContentLength (non-base64)', async () => { + const dataUrl = 'data:text/plain,' + 'X'.repeat(4096); + + const bareAxios = axios.create({ adapter: 'fetch' }); + + await assert.rejects( + bareAxios.get(dataUrl, { maxContentLength: 16 }), + (err) => { + assert.strictEqual(err.code, 'ERR_BAD_RESPONSE'); + assert.match(err.message, /maxContentLength size of 16 exceeded/); + return true; + } + ); + }); + + it('should allow a response at or below maxContentLength', async () => { + const payload = 'ok'; + const server = await startHTTPServer( + (req, res) => { + res.end(payload); + }, + { port: SERVER_PORT } + ); + + try { + const { data } = await fetchAxios.get(`${LOCAL_SERVER_URL}/`, { + maxContentLength: 1024, + }); + assert.strictEqual(data, payload); + } finally { + await stopHTTPServer(server); + } + }); + + it('should allow a body at or below maxBodyLength', async () => { + const payload = 'hello'; + let received; + const server = await startHTTPServer( + (req, res) => { + const chunks = []; + req.on('data', (c) => chunks.push(c)); + req.on('end', () => { + received = Buffer.concat(chunks).toString(); + res.end('ok'); + }); + }, + { port: SERVER_PORT } + ); + + try { + await fetchAxios.post(`${LOCAL_SERVER_URL}/`, payload, { + maxBodyLength: 1024, + }); + assert.strictEqual(received, payload); + } finally { + await stopHTTPServer(server); + } + }); + }); + describe('capability probe cleanup', () => { it('should cancel the ReadableStream created during the request stream probe', () => { // The fetch adapter factory probes for request-stream support by creating
Vulnerability mechanics
Root cause
"The fetch adapter in Axios did not enforce configured request and response size limits."
Attack vector
An attacker can exploit this vulnerability by sending requests with bodies larger than the configured `maxBodyLength` or by requesting oversized responses, bypassing the `maxContentLength` limit. This is particularly concerning in server-side applications that rely on these limits as a security boundary. The attack can be facilitated by using the `fetch` adapter, either explicitly configured or as a fallback in environments lacking `xhr` or `http` adapters. Large `data:` URLs can also be used to trigger oversized responses [ref_id=1].
Affected code
In vulnerable versions, `lib/adapters/fetch.js` failed to destructure and apply `maxContentLength` and `maxBodyLength` from the request configuration. The adapter would dispatch the `fetch()` request and then materialize the response using methods like `text()`, `arrayBuffer()`, or `blob()` without checking against the configured response size limits [ref_id=1].
What the fix does
The fix, applied in commit `e5540dc`, modifies `lib/adapters/fetch.js` to correctly read and enforce `maxContentLength` and `maxBodyLength` configurations. It introduces upfront checks for `data:` URL sizes, outbound body sizes before dispatch, and `Content-Length` for responses. Additionally, it implements streaming response enforcement and fallback checks for environments lacking `ReadableStream` [ref_id=1].
Preconditions
- configAxios must be configured with finite values for `maxContentLength` or `maxBodyLength`.
- configThe `fetch` adapter must be selected, either explicitly via `adapter: 'fetch'`, as part of an adapter array where `fetch` is chosen, or implicitly in environments where `xhr` and `http` are unavailable.
Reproduction
```javascript import http from 'node:http'; import axios from 'axios';
const server = http.createServer((req, res) => { let received = 0;
req.on('data', chunk => { received += chunk.length; });
req.on('end', () => { res.end(JSON.stringify({ received })); }); });
await new Promise(resolve => server.listen(0, resolve)); const url = `http://127.0.0.1:${server.address().port}/`;
await axios.post(url, 'A'.repeat(2 * 1024 * 1024), { adapter: 'fetch', maxBodyLength: 1024 });
// Vulnerable versions succeed and the server receives 2097152 bytes. // Fixed versions reject with ERR_BAD_REQUEST.
server.close(); ```
Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
1- Axios: Four High-Severity Vulnerabilities Disclosed Together on June 4thVypr Intelligence · Jun 4, 2026