2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00

fix: clean up error handling, fix a proto-pollution gap, and seal a few loose ends (#10922)

* Clean up error handling, fix a proto-pollution gap, and seal a few loose ends.

Been poking around the codebase and found a handful of things that needed tidying up:

- resolveConfig.js - config.params and config.paramsSerializer were being accessed directly off user input instead of going through the own() guard. If someone crafted a config with inherited params from the prototype, you'd get unexpected behavior. Swapped to own('params') / own('paramsSerializer') like the rest of the module does.

- http.js - there was a stray console.warn('emit error', err) in an abort-event catch block. Debug leftover, shouldn't reach production. Replaced with a quiet catch.

- AxiosHeaders.js - three places were throwing raw Error or TypeError instead of AxiosError. Swapped them over with ERR_BAD_OPTION_VALUE. Also added the missing AxiosError import (creates a circular dep with AxiosError.js which imports AxiosHeaders, but it works fine at runtime since the throws are inside method bodies, not at module eval time).

- toFormData.js - the circular reference detection was throwing a bare Error. Changed to AxiosError without a code, so it stays distinguishable from the depth-exceeded error that uses ERR_FORM_DATA_DEPTH_EXCEEDED. (There's a test that explicitly checks this distinction.)

- formDataToStream.js - two more raw throws (TypeError and Error) → AxiosError.

- buildURL.js - import self-path ../helpers/AxiosURLSearchParams.js when it lives in the same directory as the importer. Changed to ./AxiosURLSearchParams.js.

- index.d.cts - CanceledError was missing readonly name: 'CanceledError' that index.d.ts already has. Added it to keep the CJS declarations in sync.

Lint passes clean, all 770 unit tests green. Nothing breaking - all changes are either internal (no consumer-facing API change) or type-only.

* Update AxiosHeaders.js

* Update AxiosHeaders.js

* fix: revert breaking error-type changes per review feedback

Reverts AxiosError throws back to native Error/TypeError in AxiosHeaders,
formDataToStream, and toFormData to avoid breaking existing consumers
who catch by constructor name or check isAxiosError().

Adds regression tests for resolveConfig own('params')/own('paramsSerializer')
guard as requested in review.

Removes unused AxiosError imports from AxiosHeaders and formDataToStream.

* docs: add pre-release notes for config hardening

---------

Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
This commit is contained in:
Mohammad Faiz
2026-05-27 00:39:03 +05:30
committed by GitHub
parent 58d8a125bf
commit 73a7c55282
9 changed files with 47 additions and 9 deletions
+2
View File
@@ -9,6 +9,8 @@
## Bug Fixes
- **AxiosHeaders:** Silently skip empty response header names emitted by some React Native Android responses instead of throwing. (**#6959**, **#10875**)
- **Config Security:** Ignore inherited `params` and `paramsSerializer` values when resolving request config, preventing prototype-pollution gadgets from changing serialized URLs. (**#10922**)
- **Types:** Add the missing readonly `name: 'CanceledError'` declaration to CommonJS `CanceledError` typings to match the ESM declarations. (**#10922**)
- **HTTP Adapter - Auth on Redirect:** HTTP Basic credentials supplied via `config.auth` are now restored on same-origin redirects, fixing a regression caused by `follow-redirects` >= 1.15.8 that broke `POST` requests answered with a 303 Location. Cross-origin redirects continue to drop credentials, preserving the existing T-R2 mitigation in `THREATMODEL.md`. (**#6929**)
- **HTTP Adapter - Socket Path:** Ignore inherited `socketPath` and `allowedSocketPaths` config values when building Node.js requests, preventing prototype-pollution SSRF via Unix sockets. (**#10901**)
- **React Native FormData:** Clear the default `Content-Type` header for React Native `FormData` requests so Android can build multipart bodies with the correct boundary. (**#10898**)
+3 -1
View File
@@ -162,7 +162,9 @@ declare class AxiosError<T = unknown, D = any> extends Error {
static readonly ETIMEDOUT = 'ETIMEDOUT';
}
declare class CanceledError<T> extends AxiosError<T> {}
declare class CanceledError<T> extends AxiosError<T> {
readonly name: 'CanceledError';
}
declare class Axios {
constructor(config?: axios.AxiosRequestConfig);
+1 -1
View File
@@ -481,7 +481,7 @@ export default isHttpAdapterSupported &&
!reason || reason.type ? new CanceledError(null, config, req) : reason
);
} catch (err) {
console.warn('emit error', err);
// ignore emit errors
}
}
+1 -1
View File
@@ -117,7 +117,7 @@ class AxiosHeaders {
key;
for (const entry of header) {
if (!utils.isArray(entry)) {
throw TypeError('Object iterator must return a key-value pair');
throw new TypeError('Object iterator must return a key-value pair');
}
obj[(key = entry[0])] = (dest = obj[key])
+1 -1
View File
@@ -1,7 +1,7 @@
'use strict';
import utils from '../utils.js';
import AxiosURLSearchParams from '../helpers/AxiosURLSearchParams.js';
import AxiosURLSearchParams from './AxiosURLSearchParams.js';
/**
* It replaces URL-encoded forms of `:`, `$`, `,`, and spaces with
+2 -2
View File
@@ -73,11 +73,11 @@ const formDataToStream = (form, headersHandler, options) => {
} = options || {};
if (!utils.isFormData(form)) {
throw TypeError('FormData instance required');
throw new TypeError('FormData instance required');
}
if (boundary.length < 1 || boundary.length > 70) {
throw Error('boundary must be 1-70 characters long');
throw new Error('boundary must be 1-70 characters long');
}
const boundaryBytes = textEncoder.encode('--' + boundary + CRLF);
+2 -2
View File
@@ -56,8 +56,8 @@ function resolveConfig(config) {
newConfig.url = buildURL(
buildFullPath(baseURL, url, allowAbsoluteUrls),
config.params,
config.paramsSerializer
own('params'),
own('paramsSerializer')
);
// HTTP basic authentication
+1 -1
View File
@@ -219,7 +219,7 @@ function toFormData(obj, formData, options) {
}
if (stack.indexOf(value) !== -1) {
throw Error('Circular reference detected in ' + path.join('.'));
throw new Error('Circular reference detected in ' + path.join('.'));
}
stack.push(value);
+34
View File
@@ -7,6 +7,7 @@ import mergeConfig from '../../lib/core/mergeConfig.js';
import defaults from '../../lib/defaults/index.js';
import AxiosError from '../../lib/core/AxiosError.js';
import AxiosHeaders from '../../lib/core/AxiosHeaders.js';
import resolveConfig from '../../lib/helpers/resolveConfig.js';
import axios from '../../index.js';
describe('Prototype Pollution Protection', () => {
@@ -690,6 +691,39 @@ describe('Prototype Pollution Protection', () => {
}, 10000);
});
describe('resolveConfig params and paramsSerializer gadget', () => {
it('should not inherit polluted params via resolveConfig', () => {
Object.prototype.params = { injected: 'yes' };
try {
const resolved = resolveConfig({ url: '/api', method: 'get' });
assert.ok(resolved.url.indexOf('injected') === -1, 'polluted params must not appear in URL');
assert.strictEqual(resolved.url, '/api', 'URL must remain unchanged');
} finally {
delete Object.prototype.params;
}
});
it('should not invoke polluted paramsSerializer via resolveConfig', () => {
let serializerInvoked = false;
Object.prototype.paramsSerializer = function polluted() {
serializerInvoked = true;
return 'injected=yes';
};
try {
const resolved = resolveConfig({ url: '/api', method: 'get', params: { legit: 'true' } });
assert.strictEqual(serializerInvoked, false, 'polluted paramsSerializer must not be called');
// The URL should have legit param serialized normally
assert.ok(resolved.url.indexOf('legit=true') !== -1, 'legitimate params must still be serialized');
} finally {
delete Object.prototype.paramsSerializer;
}
});
});
// Structural defense: mergeConfig returns a null-prototype object, so any
// property read that is not an own property of config cannot inherit from
// Object.prototype. Adding a new key to Object.prototype must never appear