Skip to content

Commit b0c632f

Browse files
authored
fix: backport security issues (#10764)
* fix: header security issues * chore: dont allow passing on any error * chore backport all security fixes from v0.x * fix: cubic feedback * fix: test failing * fix: deleting call * chore: update docs
1 parent b52187f commit b0c632f

12 files changed

Lines changed: 2199 additions & 1398 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,18 @@ These are the available config options for making requests. Only the `url` is re
330330

331331
// `params` are the URL parameters to be sent with the request
332332
// Must be a plain object or a URLSearchParams object
333+
// Null bytes in param values stay percent-encoded as `%00` in the resulting query string
334+
// (GHSA-xhjh-pmcv-23jw) — Axios does not reverse `encodeURIComponent` output for `%00`,
335+
// so null-byte injection cannot be smuggled through the serializer.
333336
params: {
334337
ID: 12345
335338
},
336339

337340
// `paramsSerializer` is an optional config in charge of serializing `params`
341+
// Nested objects are walked with a bounded recursion depth (GHSA-62hf-57xw-28j9):
342+
// once `maxDepth` is exceeded the serializer throws `ERR_FORM_DATA_DEPTH_EXCEEDED`
343+
// instead of overflowing the call stack. The same cap applies to `toFormData` when
344+
// `Content-Type: multipart/form-data` triggers automatic FormData serialization.
338345
paramsSerializer: {
339346
indexes: null, // array indexes format (null - no brackets, false - empty brackets, true - brackets with indexes)
340347
maxDepth: 100 // maximum recursion depth for nested params (default: 100, Infinity disables the limit)
@@ -395,6 +402,9 @@ These are the available config options for making requests. Only the `url` is re
395402
xsrfHeaderName: 'X-XSRF-TOKEN', // default
396403

397404
// `undefined` (default) - set XSRF header only for the same origin requests
405+
// Only an explicit `true` (own property on the config) will add the XSRF header for
406+
// cross-origin requests. Values inherited from `Object.prototype` are ignored
407+
// (GHSA-xx6v-rp6x-q39c), so a polluted prototype cannot silently enable the token.
398408
withXSRFToken: boolean | undefined | ((config: AxiosRequestConfig) => boolean | undefined),
399409

400410
// `onUploadProgress` allows handling of progress events for uploads
@@ -410,9 +420,13 @@ These are the available config options for making requests. Only the `url` is re
410420
},
411421

412422
// `maxContentLength` defines the max size of the http response content in bytes allowed in node.js
423+
// Also enforced on streamed responses (`responseType: 'stream'`): bytes are counted as they
424+
// arrive and the stream is aborted with an error once the cap is exceeded (GHSA-vf2m-468p-8v99).
413425
maxContentLength: 2000,
414426

415427
// `maxBodyLength` (Node only option) defines the max size of the http request content in bytes allowed
428+
// Also enforced on stream uploads: uploaded bytes are tracked and the request is aborted
429+
// once the cap is exceeded, even when the native http transport is used directly.
416430
maxBodyLength: 2000,
417431

418432
// `validateStatus` defines whether to resolve or reject the promise for a given
@@ -599,6 +613,8 @@ instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
599613
600614
Config will be merged with an order of precedence. The order is library defaults found in [lib/defaults.js](https://github.com/axios/axios/blob/master/lib/defaults/index.js#L28), then `defaults` property of the instance, and finally `config` argument for the request. The latter will take precedence over the former. Here's an example.
601615
616+
> Note: the merged config object is created with a null prototype (`Object.create(null)`) and only own properties of the inputs are copied across. A polluted `Object.prototype` cannot leak values (for example `transport`, `adapter`, or `transformRequest`) into the outgoing config through inheritance, and keys such as `__proto__`, `constructor`, and `prototype` are dropped during the merge.
617+
602618
```js
603619
// Create an instance using the config defaults provided by the library
604620
// At this point the timeout config value is `0` as is the default for the library

‎lib/adapters/http.js‎

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,47 @@ module.exports = function httpAdapter(config) {
350350
};
351351

352352
if (responseType === 'stream') {
353-
response.data = responseStream;
354-
settle(resolve, reject, response);
353+
// Enforce maxContentLength on streamed responses too (GHSA-vf2m-468p-8v99).
354+
// Previously the stream path bypassed the size guard because the check only
355+
// ran on the buffering branch.
356+
if (config.maxContentLength > -1) {
357+
var maxContentLength = config.maxContentLength;
358+
var streamedBytes = 0;
359+
var limiter = new stream.Transform({
360+
transform: function transformChunk(chunk, encoding, callback) {
361+
streamedBytes += chunk.length;
362+
if (streamedBytes > maxContentLength) {
363+
callback(new AxiosError(
364+
'maxContentLength size of ' + maxContentLength + ' exceeded',
365+
AxiosError.ERR_BAD_RESPONSE,
366+
config,
367+
lastRequest
368+
));
369+
return;
370+
}
371+
callback(null, chunk);
372+
}
373+
});
374+
limiter.on('error', function handleLimiterError() {
375+
rejected = true;
376+
responseStream.destroy();
377+
});
378+
responseStream.on('error', function forwardError(err) {
379+
limiter.destroy(err);
380+
});
381+
response.data = limiter;
382+
settle(resolve, reject, response);
383+
// Defer piping via setImmediate so the caller's `.then` (a microtask)
384+
// has run and attached any `error`/`data` listeners before chunks flow
385+
// through the transform. `process.nextTick` would drain before those
386+
// microtasks and lose the error event.
387+
setImmediate(function startPipe() {
388+
responseStream.pipe(limiter);
389+
});
390+
} else {
391+
response.data = responseStream;
392+
settle(resolve, reject, response);
393+
}
355394
} else {
356395
var responseBuffer = [];
357396
var totalResponseBytes = 0;
@@ -476,7 +515,42 @@ module.exports = function httpAdapter(config) {
476515
if (utils.isStream(data)) {
477516
data.on('error', function handleStreamError(err) {
478517
reject(AxiosError.from(err, config, null, req));
479-
}).pipe(req);
518+
});
519+
520+
// follow-redirects enforces options.maxBodyLength for stream uploads, but the
521+
// native http/https transport (used when maxRedirects === 0) does not.
522+
// Count bytes ourselves so the limit is always honored (GHSA-5c9x-8gcm-mpgx).
523+
var nativeTransport = transport === http || transport === https;
524+
if (nativeTransport && config.maxBodyLength > -1) {
525+
var maxBodyLength = config.maxBodyLength;
526+
var uploadedBytes = 0;
527+
var bodyLimiter = new stream.Transform({
528+
transform: function transformChunk(chunk, encoding, callback) {
529+
uploadedBytes += chunk.length;
530+
if (uploadedBytes > maxBodyLength) {
531+
callback(new AxiosError(
532+
'Request body larger than maxBodyLength limit',
533+
AxiosError.ERR_BAD_REQUEST,
534+
config,
535+
req
536+
));
537+
return;
538+
}
539+
callback(null, chunk);
540+
}
541+
});
542+
bodyLimiter.on('error', function handleLimiterError(err) {
543+
if (rejected) return;
544+
rejected = true;
545+
try { data.unpipe(bodyLimiter); } catch (e) { /* noop */ }
546+
try { bodyLimiter.unpipe(req); } catch (e) { /* noop */ }
547+
req.destroy();
548+
reject(err);
549+
});
550+
data.pipe(bodyLimiter).pipe(req);
551+
} else {
552+
data.pipe(req);
553+
}
480554
} else {
481555
req.end(data);
482556
}

‎lib/adapters/xhr.js‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ module.exports = function xhrAdapter(config) {
1818
var requestData = config.data;
1919
var requestHeaders = config.headers;
2020
var responseType = config.responseType;
21-
var withXSRFToken = config.withXSRFToken;
21+
// Guard against prototype pollution (GHSA-xx6v-rp6x-q39c): only honor own properties.
22+
var withXSRFToken = utils.hasOwnProperty(config, 'withXSRFToken') ? config.withXSRFToken : undefined;
2223
var onCanceled;
2324
function done() {
2425
if (config.cancelToken) {
@@ -146,8 +147,11 @@ module.exports = function xhrAdapter(config) {
146147
// Specifically not if we're in a web worker, or react-native.
147148
if (utils.isStandardBrowserEnv()) {
148149
// Add xsrf header
149-
withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(config));
150-
if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(fullPath))) {
150+
if (utils.isFunction(withXSRFToken)) {
151+
withXSRFToken = withXSRFToken(config);
152+
}
153+
// Strict boolean check (GHSA-xx6v-rp6x-q39c): only `true` short-circuits the same-origin guard.
154+
if (withXSRFToken === true || (withXSRFToken !== false && isURLSameOrigin(fullPath))) {
151155
// Add xsrf header
152156
var xsrfValue = config.xsrfHeaderName && config.xsrfCookieName && cookies.read(config.xsrfCookieName);
153157
if (xsrfValue) {

‎lib/core/mergeConfig.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ var utils = require('../utils');
1313
module.exports = function mergeConfig(config1, config2) {
1414
// eslint-disable-next-line no-param-reassign
1515
config2 = config2 || {};
16-
var config = {};
16+
// Use a null-prototype object so a polluted Object.prototype cannot leak
17+
// values (e.g. transport, adapter) into the returned config via inheritance.
18+
var config = Object.create(null);
1719

1820
function getOwn(source, prop) {
1921
return utils.hasOwnProperty(source, prop) ? source[prop] : undefined;

‎lib/helpers/AxiosURLSearchParams.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
var toFormData = require('./toFormData');
44

55
function encode(str) {
6+
// Do not map `%00` back to a raw null byte (GHSA-xhjh-pmcv-23jw): that reversed
7+
// the safe percent-encoding from encodeURIComponent and enabled null byte injection.
68
var charMap = {
79
'!': '%21',
810
"'": '%27',

‎lib/helpers/toFormData.js‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,12 @@ function toFormData(obj, formData, options) {
149149
function build(value, path, depth) {
150150
if (utils.isUndefined(value)) return;
151151

152+
// eslint-disable-next-line no-param-reassign
152153
depth = depth || 0;
153154

154155
if (depth > maxDepth) {
155156
throw new AxiosError(
156-
'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
157+
'Maximum object depth of ' + maxDepth + ' exceeded (got ' + depth + ' levels)',
157158
AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
158159
);
159160
}
@@ -181,7 +182,7 @@ function toFormData(obj, formData, options) {
181182
throw new TypeError('data must be an object');
182183
}
183184

184-
build(obj);
185+
build(obj, null, 0);
185186

186187
return formData;
187188
}

‎lib/utils.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ function isFormData(thing) {
208208
var pattern = '[object FormData]';
209209
if (!thing) return false;
210210
if (typeof FormData === 'function' && thing instanceof FormData) return true;
211+
// Reject non-objects (strings, numbers, booleans) up front — Object.getPrototypeOf
212+
// throws a TypeError on primitives in ES5 environments.
213+
if (!isObject(thing)) return false;
211214
// Reject plain objects inheriting directly from Object.prototype so prototype-pollution gadgets can't spoof FormData (GHSA-6chq-wfr3-2hj9).
212215
var proto = Object.getPrototypeOf(thing);
213216
if (!proto || proto === Object.prototype) return false;

0 commit comments

Comments
 (0)