mirror of
https://github.com/tenrok/axios.git
synced 2026-06-20 20:00:40 +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:
@@ -9,6 +9,8 @@
|
|||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
- **AxiosHeaders:** Silently skip empty response header names emitted by some React Native Android responses instead of throwing. (**#6959**, **#10875**)
|
- **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 - 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**)
|
- **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**)
|
- **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
@@ -162,7 +162,9 @@ declare class AxiosError<T = unknown, D = any> extends Error {
|
|||||||
static readonly ETIMEDOUT = 'ETIMEDOUT';
|
static readonly ETIMEDOUT = 'ETIMEDOUT';
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class CanceledError<T> extends AxiosError<T> {}
|
declare class CanceledError<T> extends AxiosError<T> {
|
||||||
|
readonly name: 'CanceledError';
|
||||||
|
}
|
||||||
|
|
||||||
declare class Axios {
|
declare class Axios {
|
||||||
constructor(config?: axios.AxiosRequestConfig);
|
constructor(config?: axios.AxiosRequestConfig);
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ export default isHttpAdapterSupported &&
|
|||||||
!reason || reason.type ? new CanceledError(null, config, req) : reason
|
!reason || reason.type ? new CanceledError(null, config, req) : reason
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('emit error', err);
|
// ignore emit errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class AxiosHeaders {
|
|||||||
key;
|
key;
|
||||||
for (const entry of header) {
|
for (const entry of header) {
|
||||||
if (!utils.isArray(entry)) {
|
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])
|
obj[(key = entry[0])] = (dest = obj[key])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import utils from '../utils.js';
|
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
|
* It replaces URL-encoded forms of `:`, `$`, `,`, and spaces with
|
||||||
|
|||||||
@@ -73,11 +73,11 @@ const formDataToStream = (form, headersHandler, options) => {
|
|||||||
} = options || {};
|
} = options || {};
|
||||||
|
|
||||||
if (!utils.isFormData(form)) {
|
if (!utils.isFormData(form)) {
|
||||||
throw TypeError('FormData instance required');
|
throw new TypeError('FormData instance required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boundary.length < 1 || boundary.length > 70) {
|
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);
|
const boundaryBytes = textEncoder.encode('--' + boundary + CRLF);
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ function resolveConfig(config) {
|
|||||||
|
|
||||||
newConfig.url = buildURL(
|
newConfig.url = buildURL(
|
||||||
buildFullPath(baseURL, url, allowAbsoluteUrls),
|
buildFullPath(baseURL, url, allowAbsoluteUrls),
|
||||||
config.params,
|
own('params'),
|
||||||
config.paramsSerializer
|
own('paramsSerializer')
|
||||||
);
|
);
|
||||||
|
|
||||||
// HTTP basic authentication
|
// HTTP basic authentication
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ function toFormData(obj, formData, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stack.indexOf(value) !== -1) {
|
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);
|
stack.push(value);
|
||||||
|
|||||||
+34
@@ -7,6 +7,7 @@ import mergeConfig from '../../lib/core/mergeConfig.js';
|
|||||||
import defaults from '../../lib/defaults/index.js';
|
import defaults from '../../lib/defaults/index.js';
|
||||||
import AxiosError from '../../lib/core/AxiosError.js';
|
import AxiosError from '../../lib/core/AxiosError.js';
|
||||||
import AxiosHeaders from '../../lib/core/AxiosHeaders.js';
|
import AxiosHeaders from '../../lib/core/AxiosHeaders.js';
|
||||||
|
import resolveConfig from '../../lib/helpers/resolveConfig.js';
|
||||||
import axios from '../../index.js';
|
import axios from '../../index.js';
|
||||||
|
|
||||||
describe('Prototype Pollution Protection', () => {
|
describe('Prototype Pollution Protection', () => {
|
||||||
@@ -690,6 +691,39 @@ describe('Prototype Pollution Protection', () => {
|
|||||||
}, 10000);
|
}, 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
|
// Structural defense: mergeConfig returns a null-prototype object, so any
|
||||||
// property read that is not an own property of config cannot inherit from
|
// 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
|
// Object.prototype. Adding a new key to Object.prototype must never appear
|
||||||
|
|||||||
Reference in New Issue
Block a user