High severity7.4GHSA Advisory· Published May 8, 2026· Updated May 13, 2026
CVE-2026-42264
CVE-2026-42264
Description
Axios is a promise based HTTP client for the browser and Node.js. From version 1.0.0 to before version 1.15.2, fFive config properties (auth, baseURL, socketPath, beforeRedirect, and insecureHTTPParser) in the HTTP adapter are read via direct property access without hasOwnProperty guards, making them exploitable as prototype pollution gadgets. When Object.prototype is polluted by another dependency in the same process, axios silently picks up these polluted values on every outbound HTTP request. This issue has been patched in version 1.15.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
axiosnpm | >= 1.0.0, < 1.15.2 | 1.15.2 |
Affected products
2Patches
147915144662ffix: more header pollutions (#10779)
6 files changed · +601 −59
CHANGELOG.md+44 −44 modified@@ -6,35 +6,35 @@ This release delivers two critical security patches targeting header injection a ## 🔒 Security Fixes -* **Header Injection (CRLF):** Rejects any header value containing `\r` or `\n` characters to block CRLF injection chains that could be used to exfiltrate cloud metadata (IMDS). Behavior change: headers with CR/LF now throw `"Invalid character in header content"`. (__#10660__) +- **Header Injection (CRLF):** Rejects any header value containing `\r` or `\n` characters to block CRLF injection chains that could be used to exfiltrate cloud metadata (IMDS). Behavior change: headers with CR/LF now throw `"Invalid character in header content"`. (**#10660**) -* **SSRF via `no_proxy` Bypass:** Introduces a `shouldBypassProxy` helper that normalises hostnames (strips trailing dots, handles bracketed IPv6) before evaluating `no_proxy`/`NO_PROXY` rules, closing a gap that could cause loopback or internal hosts to be inadvertently proxied. (__#10661__) +- **SSRF via `no_proxy` Bypass:** Introduces a `shouldBypassProxy` helper that normalises hostnames (strips trailing dots, handles bracketed IPv6) before evaluating `no_proxy`/`NO_PROXY` rules, closing a gap that could cause loopback or internal hosts to be inadvertently proxied. (**#10661**) ## 🚀 New Features -* **Deno & Bun Runtime Support:** Added full smoke test suites for Deno and Bun, with CI workflows that run both runtimes before any release is cut. (__#10652__) +- **Deno & Bun Runtime Support:** Added full smoke test suites for Deno and Bun, with CI workflows that run both runtimes before any release is cut. (**#10652**) ## 🐛 Bug Fixes -* **Node.js v22 Compatibility:** Replaced deprecated `url.parse()` calls with the WHATWG `URL`/`URLSearchParams` API across examples, sandbox, and tests, eliminating `DEP0169` deprecation warnings on Node.js v22+. (__#10625__) +- **Node.js v22 Compatibility:** Replaced deprecated `url.parse()` calls with the WHATWG `URL`/`URLSearchParams` API across examples, sandbox, and tests, eliminating `DEP0169` deprecation warnings on Node.js v22+. (**#10625**) ## 🔧 Maintenance & Chores -* **CI Security Hardening:** Added [zizmor](https://github.com/zizmorcore/zizmor) GitHub Actions security scanner; switched npm publish to OIDC Trusted Publishing (removing the long-lived `NODE_AUTH_TOKEN`); pinned all action references to full commit SHAs; narrowed workflow permissions to least privilege; gated the publish step behind a dedicated `npm-publish` environment; and blocked the sponsor-block workflow from running on forks. (__#10618__, __#10619__, __#10627__, __#10637__, __#10641__, __#10666__) +- **CI Security Hardening:** Added [zizmor](https://github.com/zizmorcore/zizmor) GitHub Actions security scanner; switched npm publish to OIDC Trusted Publishing (removing the long-lived `NODE_AUTH_TOKEN`); pinned all action references to full commit SHAs; narrowed workflow permissions to least privilege; gated the publish step behind a dedicated `npm-publish` environment; and blocked the sponsor-block workflow from running on forks. (**#10618**, **#10619**, **#10627**, **#10637**, **#10641**, **#10666**) -* **Docs:** Clarified HTTP/2 support and the unsupported `httpVersion` option; added documentation for header case preservation; improved the `beforeRedirect` example to prevent accidental credential leakage. (__#10644__, __#10654__, __#10624__) +- **Docs:** Clarified HTTP/2 support and the unsupported `httpVersion` option; added documentation for header case preservation; improved the `beforeRedirect` example to prevent accidental credential leakage. (**#10644**, **#10654**, **#10624**) -* **Dependencies:** Bumped `picomatch`, `handlebars`, `serialize-javascript`, `vite` (×3), `denoland/setup-deno`, and 4 additional dev dependencies to latest versions. (__#10564__, __#10565__, __#10567__, __#10568__, __#10572__, __#10574__, __#10663__, __#10664__, __#10665__, __#10669__, __#10670__) +- **Dependencies:** Bumped `picomatch`, `handlebars`, `serialize-javascript`, `vite` (×3), `denoland/setup-deno`, and 4 additional dev dependencies to latest versions. (**#10564**, **#10565**, **#10567**, **#10568**, **#10572**, **#10574**, **#10663**, **#10664**, **#10665**, **#10669**, **#10670**) ## 🌟 New Contributors We are thrilled to welcome our new contributors. Thank you for helping improve axios: -* **@Kilros0817** (__#10625__) -* **@shaanmajid** (__#10616__, __#10617__, __#10618__, __#10619__, __#10637__, __#10641__, __#10666__) -* **@ashstrc** (__#10624__, __#10644__) -* **@Abhi3975** (__#10589__) -* **@raashish1601** (__#10573__) +- **@Kilros0817** (**#10625**) +- **@shaanmajid** (**#10616**, **#10617**, **#10618**, **#10619**, **#10637**, **#10641**, **#10666**) +- **@ashstrc** (**#10624**, **#10644**) +- **@Abhi3975** (**#10589**) +- **@raashish1601** (**#10573**) [Full Changelog](https://github.com/axios/axios/compare/v1.14.0...v1.15.0) @@ -46,33 +46,33 @@ This release fixes a security vulnerability in the `formidable` dependency, reso ## 🔒 Security Fixes -* **Formidable Vulnerability:** Upgraded `formidable` from v2 to v3 to address a reported arbitrary-file vulnerability. Updated test server and assertions to align with the v3 API. (__#7533__) +- **Formidable Vulnerability:** Upgraded `formidable` from v2 to v3 to address a reported arbitrary-file vulnerability. Updated test server and assertions to align with the v3 API. (**#7533**) ## 🐛 Bug Fixes -* **CommonJS Compatibility:** Restored `require('axios')` in Node.js by correcting the `main` field in `package.json` to point to the built CJS bundle. (__#7532__) +- **CommonJS Compatibility:** Restored `require('axios')` in Node.js by correcting the `main` field in `package.json` to point to the built CJS bundle. (**#7532**) -* **Fetch Adapter:** Cancel the `ReadableStream` body after the request stream capability probe to prevent resource leaks. (__#7515__) +- **Fetch Adapter:** Cancel the `ReadableStream` body after the request stream capability probe to prevent resource leaks. (**#7515**) -* **Proxy:** Upgraded `proxy-from-env` to v2 and switched to the named `getProxyForUrl` export, fixing proxy detection from environment variables and resolving CJS bundling errors. (__#7499__) +- **Proxy:** Upgraded `proxy-from-env` to v2 and switched to the named `getProxyForUrl` export, fixing proxy detection from environment variables and resolving CJS bundling errors. (**#7499**) -* **HTTP/2:** Close detached HTTP/2 sessions on timeout to free resources when no new requests arrive. (__#7457__) +- **HTTP/2:** Close detached HTTP/2 sessions on timeout to free resources when no new requests arrive. (**#7457**) -* **Headers:** Trim trailing CRLF characters from normalised header values. (__#7456__) +- **Headers:** Trim trailing CRLF characters from normalised header values. (**#7456**) ## 🔧 Maintenance & Chores -* **Toolchain Modernisation:** Migrated test suite to Vitest, updated ESLint to v10, upgraded Rollup and `@rollup/plugin-babel`, migrated to Husky 9, upgraded TypeScript to latest, and modernised the Express test harness. (__#7484__, __#7489__, __#7498__, __#7505__, __#7506__, __#7507__, __#7508__, __#7509__, __#7510__, __#7516__, __#7522__) +- **Toolchain Modernisation:** Migrated test suite to Vitest, updated ESLint to v10, upgraded Rollup and `@rollup/plugin-babel`, migrated to Husky 9, upgraded TypeScript to latest, and modernised the Express test harness. (**#7484**, **#7489**, **#7498**, **#7505**, **#7506**, **#7507**, **#7508**, **#7509**, **#7510**, **#7516**, **#7522**) -* **Dependencies:** Bumped `multer` to v2, `minimatch`, `tar`, `pacote`, `@babel/preset-env`, and additional dev dependencies. (__#7453__, __#7480__, __#7491__, __#7504__, __#7517__, __#7531__) +- **Dependencies:** Bumped `multer` to v2, `minimatch`, `tar`, `pacote`, `@babel/preset-env`, and additional dev dependencies. (**#7453**, **#7480**, **#7491**, **#7504**, **#7517**, **#7531**) ## 🌟 New Contributors We are thrilled to welcome our new contributors. Thank you for helping improve axios: -* **@penkzhou** (__#7515__) -* **@aviu16** (__#7456__) -* **@fedotov** (__#7457__) +- **@penkzhou** (**#7515**) +- **@aviu16** (**#7456**) +- **@fedotov** (**#7457**) [Full Changelog](https://github.com/axios/axios/compare/v1.13.6...v1.14.0) @@ -84,31 +84,31 @@ This release adds React Native Blob support, fixes several enumeration and expor ## 🚀 New Features -* **React Native Blob Support:** Axios now correctly handles native Blob objects in React Native environments. (__#5764__) +- **React Native Blob Support:** Axios now correctly handles native Blob objects in React Native environments. (**#5764**) ## 🐛 Bug Fixes -* **AxiosError:** Fixed `AxiosError.from` not copying the `status` field from the source error. (__#7403__) +- **AxiosError:** Fixed `AxiosError.from` not copying the `status` field from the source error. (**#7403**) -* **AxiosError:** Made the `message` property enumerable so it appears in `JSON.stringify` output and `Object.keys`. (__#7392__) +- **AxiosError:** Made the `message` property enumerable so it appears in `JSON.stringify` output and `Object.keys`. (**#7392**) -* **FormData Detection:** Corrected safe FormData detection for WeChat Mini Program environments. (__#7324__) +- **FormData Detection:** Corrected safe FormData detection for WeChat Mini Program environments. (**#7324**) -* **React Native / Browserify Export:** Fixed broken module export that caused import failures in React Native and Browserify. (__#7386__) +- **React Native / Browserify Export:** Fixed broken module export that caused import failures in React Native and Browserify. (**#7386**) ## 🔧 Maintenance & Chores -* **Dependencies:** Migrated `@rollup/plugin-babel` from v5 to v6 and bumped the development dependencies group. (__#7424__, __#7432__) +- **Dependencies:** Migrated `@rollup/plugin-babel` from v5 to v6 and bumped the development dependencies group. (**#7424**, **#7432**) ## 🌟 New Contributors We are thrilled to welcome our new contributors. Thank you for helping improve axios: -* **@moh3n9595** (__#5764__) -* **@skrtheboss** (__#7403__) -* **@ybbus** (__#7392__) -* **@Shiwaangee** (__#7324__) -* **@Gudahtt** (__#7386__) +- **@moh3n9595** (**#5764**) +- **@skrtheboss** (**#7403**) +- **@ybbus** (**#7392**) +- **@Shiwaangee** (**#7324**) +- **@Gudahtt** (**#7386**) [Full Changelog](https://github.com/axios/axios/compare/v1.13.5...v1.13.6) @@ -120,29 +120,29 @@ This release patches a prototype pollution denial-of-service vulnerability, fixe ## 🔒 Security Fixes -* **Prototype Pollution (DoS):** Hardened `mergeConfig` to ignore `__proto__`, `constructor`, and `prototype` keys, preventing denial-of-service via prototype pollution when merging user-supplied config. (__#7369__) +- **Prototype Pollution (DoS):** Hardened `mergeConfig` to ignore `__proto__`, `constructor`, and `prototype` keys, preventing denial-of-service via prototype pollution when merging user-supplied config. (**#7369**) ## 🚀 New Features -* **`isAbsoluteURL` Validation:** Added input validation to `isAbsoluteURL` to handle malformed or unexpected input gracefully. (__#7326__) +- **`isAbsoluteURL` Validation:** Added input validation to `isAbsoluteURL` to handle malformed or unexpected input gracefully. (**#7326**) ## 🐛 Bug Fixes -* **AxiosError `status`:** Restored the `status` field on `AxiosError` instances, which was missing in v1.13.3 and later. (__#7368__) +- **AxiosError `status`:** Restored the `status` field on `AxiosError` instances, which was missing in v1.13.3 and later. (**#7368**) -* **Interceptor Ordering:** Added a `useLegacyInterceptorOrder` option to restore pre-v1.13 interceptor execution order for applications relying on the previous behaviour. ([569f028](https://github.com/axios/axios/commit/569f028a5878faaec8d7d138ba686aac407bda4c)) +- **Interceptor Ordering:** Added a `useLegacyInterceptorOrder` option to restore pre-v1.13 interceptor execution order for applications relying on the previous behaviour. ([569f028](https://github.com/axios/axios/commit/569f028a5878faaec8d7d138ba686aac407bda4c)) ## 🔧 Maintenance & Chores -* **CI:** Fixed run conditions and updated workflow YAMLs. (__#7372__, __#7373__) +- **CI:** Fixed run conditions and updated workflow YAMLs. (**#7372**, **#7373**) -* **Dependencies:** Bumped `karma-sourcemap-loader` and minor package versions. (__#7356__, __#7360__) +- **Dependencies:** Bumped `karma-sourcemap-loader` and minor package versions. (**#7356**, **#7360**) ## 🌟 New Contributors We are thrilled to welcome our new contributors. Thank you for helping improve axios: -* **@asmitha-16** (__#7326__) +- **@asmitha-16** (**#7326**) [Full Changelog](https://github.com/axios/axios/compare/v1.13.4...v1.13.5) @@ -154,13 +154,13 @@ Patch release fixing regressions introduced in v1.13.3, including TypeScript exp ## 🐛 Bug Fixes -* **v1.13.3 Regressions:** Fixed multiple issues introduced by the v1.13.3 release, including broken merge configs. (__#7352__) +- **v1.13.3 Regressions:** Fixed multiple issues introduced by the v1.13.3 release, including broken merge configs. (**#7352**) -* **TypeScript Exports:** Corrected TypeScript export declarations to restore proper type resolution. (__#4884__) +- **TypeScript Exports:** Corrected TypeScript export declarations to restore proper type resolution. (**#4884**) ## 🔧 Maintenance & Chores -* **CI & Build:** Refactored CI pipeline and build configuration for stability. (__#7340__) +- **CI & Build:** Refactored CI pipeline and build configuration for stability. (**#7340**) [Full Changelog](https://github.com/axios/axios/compare/v1.13.3...v1.13.4)
lib/adapters/http.js+17 −11 modified@@ -615,9 +615,10 @@ export default isHttpAdapterSupported && // HTTP basic authentication let auth = undefined; - if (config.auth) { - const username = config.auth.username || ''; - const password = config.auth.password || ''; + const configAuth = own('auth'); + if (configAuth) { + const username = configAuth.username || ''; + const password = configAuth.password || ''; auth = username + ':' + password; } @@ -651,7 +652,10 @@ export default isHttpAdapterSupported && false ); - const options = { + // Null-prototype to block prototype pollution gadgets on properties read + // directly by Node's http.request (e.g. insecureHTTPParser, lookup). + // See GHSA-q8qp-cvcw-x6jj. + const options = Object.assign(Object.create(null), { path, method: method, headers: headers.toJSON(), @@ -660,9 +664,9 @@ export default isHttpAdapterSupported && protocol, family, beforeRedirect: dispatchBeforeRedirect, - beforeRedirects: {}, + beforeRedirects: Object.create(null), http2Options, - }; + }); // cacheable-lookup integration hotfix !utils.isUndefined(lookup) && (options.lookup = lookup); @@ -723,8 +727,9 @@ export default isHttpAdapterSupported && if (config.maxRedirects) { options.maxRedirects = config.maxRedirects; } - if (config.beforeRedirect) { - options.beforeRedirects.config = config.beforeRedirect; + const configBeforeRedirect = own('beforeRedirect'); + if (configBeforeRedirect) { + options.beforeRedirects.config = configBeforeRedirect; } transport = isHttpsRequest ? httpsFollow : httpFollow; } @@ -737,9 +742,10 @@ export default isHttpAdapterSupported && options.maxBodyLength = Infinity; } - if (config.insecureHTTPParser) { - options.insecureHTTPParser = config.insecureHTTPParser; - } + // Always set an explicit own value so a polluted + // Object.prototype.insecureHTTPParser cannot enable the lenient parser + // through Node's internal options copy (GHSA-q8qp-cvcw-x6jj). + options.insecureHTTPParser = Boolean(own('insecureHTTPParser')); // Create the request req = transport.request(options, function handleResponse(res) {
lib/core/mergeConfig.js+12 −1 modified@@ -17,7 +17,18 @@ const headersToObject = (thing) => (thing instanceof AxiosHeaders ? { ...thing } export default function mergeConfig(config1, config2) { // eslint-disable-next-line no-param-reassign config2 = config2 || {}; - const config = {}; + + // Use a null-prototype object so that downstream reads such as `config.auth` + // or `config.baseURL` cannot inherit polluted values from Object.prototype + // (see GHSA-q8qp-cvcw-x6jj). `hasOwnProperty` is restored as a non-enumerable + // own slot to preserve ergonomics for user code that relies on it. + const config = Object.create(null); + Object.defineProperty(config, 'hasOwnProperty', { + value: Object.prototype.hasOwnProperty, + enumerable: false, + writable: true, + configurable: true, + }); function getMergedValue(target, source, prop, caseless) { if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
lib/helpers/resolveConfig.js+14 −2 modified@@ -10,12 +10,24 @@ import buildURL from './buildURL.js'; export default (config) => { const newConfig = mergeConfig({}, config); - let { data, withXSRFToken, xsrfHeaderName, xsrfCookieName, headers, auth } = newConfig; + // Read only own properties to prevent prototype pollution gadgets + // (e.g. Object.prototype.baseURL = 'https://evil.com'). See GHSA-q8qp-cvcw-x6jj. + const own = (key) => (utils.hasOwnProp(newConfig, key) ? newConfig[key] : undefined); + + const data = own('data'); + let withXSRFToken = own('withXSRFToken'); + const xsrfHeaderName = own('xsrfHeaderName'); + const xsrfCookieName = own('xsrfCookieName'); + let headers = own('headers'); + const auth = own('auth'); + const baseURL = own('baseURL'); + const allowAbsoluteUrls = own('allowAbsoluteUrls'); + const url = own('url'); newConfig.headers = headers = AxiosHeaders.from(headers); newConfig.url = buildURL( - buildFullPath(newConfig.baseURL, newConfig.url, newConfig.allowAbsoluteUrls), + buildFullPath(baseURL, url, allowAbsoluteUrls), config.params, config.paramsSerializer );
lib/helpers/validator.js+3 −1 modified@@ -86,7 +86,9 @@ function assertOptions(options, schema, allowUnknown) { let i = keys.length; while (i-- > 0) { const opt = keys[i]; - const validator = schema[opt]; + // Use hasOwnProperty so a polluted Object.prototype.<opt> cannot supply + // a non-function validator and cause a TypeError. See GHSA-q8qp-cvcw-x6jj. + const validator = Object.prototype.hasOwnProperty.call(schema, opt) ? schema[opt] : undefined; if (validator) { const value = options[opt]; const result = value === undefined || validator(value, opt, options);
tests/unit/prototypePollution.test.js+511 −0 modified@@ -21,6 +21,32 @@ describe('Prototype Pollution Protection', () => { delete Object.prototype.family; delete Object.prototype.http2Options; delete Object.prototype.validateStatus; + delete Object.prototype.auth; + delete Object.prototype.baseURL; + delete Object.prototype.socketPath; + delete Object.prototype.beforeRedirect; + delete Object.prototype.insecureHTTPParser; + delete Object.prototype.adapter; + delete Object.prototype.httpAgent; + delete Object.prototype.httpsAgent; + delete Object.prototype.proxy; + delete Object.prototype.maxContentLength; + delete Object.prototype.maxBodyLength; + delete Object.prototype.maxRedirects; + delete Object.prototype.maxRate; + delete Object.prototype.timeout; + delete Object.prototype.transitional; + delete Object.prototype.timeoutErrorMessage; + delete Object.prototype.env; + delete Object.prototype.cancelToken; + delete Object.prototype.signal; + delete Object.prototype.decompress; + delete Object.prototype.params; + delete Object.prototype.paramsSerializer; + delete Object.prototype.method; + delete Object.prototype.withCredentials; + delete Object.prototype.responseType; + delete Object.prototype.fetchOptions; }); describe('utils.merge', () => { @@ -385,4 +411,489 @@ describe('Prototype Pollution Protection', () => { } }, 10000); }); + + // GHSA-q8qp-cvcw-x6jj: five config properties were read via direct property + // access in the http adapter and resolveConfig, bypassing hasOwnProperty and + // allowing prototype pollution gadgets (auth, baseURL, socketPath, + // beforeRedirect, insecureHTTPParser). + describe('GHSA-q8qp-cvcw-x6jj http adapter gadgets', () => { + function startServer(handler) { + return new Promise((resolve) => { + const server = http.createServer(handler || ((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ headers: req.headers, url: req.url })); + })); + server.listen(0, '127.0.0.1', () => resolve(server)); + }); + } + + function stopServer(server) { + return new Promise((resolve) => server.close(resolve)); + } + + it('should not pick up Object.prototype.auth as an Authorization header', async () => { + Object.prototype.auth = { username: 'attacker', password: 'exfil' }; + + const server = await startServer(); + const { port } = server.address(); + + try { + const res = await axios.get(`http://127.0.0.1:${port}/api`); + assert.strictEqual(res.data.headers.authorization, undefined); + } finally { + await stopServer(server); + } + }, 10000); + + it('should not pick up Object.prototype.socketPath and redirect the request', async () => { + Object.prototype.socketPath = '/tmp/axios-should-never-be-used.sock'; + + const server = await startServer(); + const { port } = server.address(); + + try { + const res = await axios.get(`http://127.0.0.1:${port}/api`); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.url, '/api'); + } finally { + await stopServer(server); + } + }, 10000); + + it('should not invoke Object.prototype.beforeRedirect during redirects', async () => { + let hijackCalled = false; + Object.prototype.beforeRedirect = function polluted() { + hijackCalled = true; + }; + + const target = await startServer(); + const { port: targetPort } = target.address(); + + const redirector = await startServer((req, res) => { + res.writeHead(302, { Location: `http://127.0.0.1:${targetPort}/final` }); + res.end(); + }); + const { port: redirectorPort } = redirector.address(); + + try { + const res = await axios.get(`http://127.0.0.1:${redirectorPort}/start`); + assert.strictEqual(res.status, 200); + assert.strictEqual(hijackCalled, false); + } finally { + await stopServer(redirector); + await stopServer(target); + } + }, 10000); + + it('should not enable insecureHTTPParser via Object.prototype', async () => { + // A raw TCP server emits a response that uses LF-only line terminators + // instead of CRLF. Node's strict HTTP parser rejects this payload with + // HPE_CR_EXPECTED; the insecure parser accepts it. Verified: with an + // explicit `insecureHTTPParser: true` on the request config, this + // payload is parsed successfully — so if Object.prototype.insecureHTTPParser + // were picked up, the request would succeed. The request must fail when + // the gadget is properly blocked. + Object.prototype.insecureHTTPParser = true; + + const net = await import('net'); + const malformedPayload = + 'HTTP/1.1 200 OK\n' + + 'Content-Type: application/json\n' + + 'Content-Length: 2\n' + + '\n' + + '{}'; + const malformed = await new Promise((resolve) => { + const srv = net.createServer((socket) => { + socket.once('data', () => socket.end(malformedPayload)); + }); + srv.listen(0, '127.0.0.1', () => resolve(srv)); + }); + const { port } = malformed.address(); + + try { + let threw = false; + let caughtCode = ''; + try { + await axios.get(`http://127.0.0.1:${port}/`, { + transitional: { clarifyTimeoutError: false }, + }); + } catch (err) { + threw = true; + caughtCode = String(err && (err.code || err.message)); + } + assert.strictEqual( + threw, + true, + `request should be rejected by the strict HTTP parser (got: ${caughtCode || 'success'})` + ); + // The exact llhttp code for LF-only line terminators varies across + // Node versions (historically HPE_LF_EXPECTED, more recently + // HPE_CR_EXPECTED). Match any parser error to remain stable across + // Node releases while still confirming the strict parser rejected + // the payload. + assert.match( + caughtCode, + /^HPE_/, + `expected an HPE_* parser error, got: ${caughtCode}` + ); + } finally { + await new Promise((resolve) => malformed.close(resolve)); + } + }, 10000); + }); + + describe('GHSA-q8qp-cvcw-x6jj resolveConfig baseURL gadget', () => { + // The baseURL branch in buildFullPath only runs when the requested URL is + // relative (or allowAbsoluteUrls === false). An absolute URL would skip + // baseURL regardless of pollution and would not exercise the gadget. We + // therefore issue a relative GET and assert that either: + // - the request fails (no host to resolve) because baseURL is correctly + // absent from the merged config, OR + // - the request is fulfilled without hitting the hijacker. + // Critically, hijackHit must always be false. + it('should not hijack relative-URL requests via Object.prototype.baseURL', async () => { + let hijackHit = false; + const hijacker = http.createServer((req, res) => { + hijackHit = true; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end('{"hijacked":true}'); + }); + await new Promise((resolve) => hijacker.listen(0, '127.0.0.1', resolve)); + const { port: hijackerPort } = hijacker.address(); + + Object.prototype.baseURL = `http://127.0.0.1:${hijackerPort}`; + + try { + let threw = false; + try { + await axios.get('/api'); + } catch (_err) { + threw = true; + } + // Either the request fails (desired — no baseURL means no host) or it + // resolves, but it must NOT hit the polluted hijacker. + assert.strictEqual(hijackHit, false); + assert.strictEqual(threw, true); + } finally { + await new Promise((resolve) => hijacker.close(resolve)); + } + }, 10000); + + // Second variant using allowAbsoluteUrls: false to force the baseURL path + // even for a fully-qualified requested URL. + it('should not hijack requests via Object.prototype.baseURL with allowAbsoluteUrls:false', async () => { + let hijackHit = false; + const hijacker = http.createServer((req, res) => { + hijackHit = true; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end('{"hijacked":true}'); + }); + await new Promise((resolve) => hijacker.listen(0, '127.0.0.1', resolve)); + const { port: hijackerPort } = hijacker.address(); + + const target = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end('{"ok":true}'); + }); + await new Promise((resolve) => target.listen(0, '127.0.0.1', resolve)); + const { port: targetPort } = target.address(); + + Object.prototype.baseURL = `http://127.0.0.1:${hijackerPort}`; + + try { + // If the gadget were picked up, combineURLs(hijacker, `http://target`) + // would route to the hijacker. It must not. + let threw = false; + try { + await axios.get(`http://127.0.0.1:${targetPort}/api`, { + allowAbsoluteUrls: false, + }); + } catch (_err) { + threw = true; + } + assert.strictEqual(hijackHit, false); + // allowAbsoluteUrls:false + no baseURL → combineURLs not invoked + // (baseURL falsy) → returns requested URL as-is → target receives it. + // If baseURL were inherited from prototype, it would be truthy and + // combineURLs would be invoked, routing to the hijacker. + assert.strictEqual(threw, false); + } finally { + await new Promise((resolve) => hijacker.close(resolve)); + await new Promise((resolve) => target.close(resolve)); + } + }, 10000); + }); + + // 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 + // as a property of the merged config. + describe('mergeConfig null-prototype structural defense', () => { + it('should return an object whose prototype is null', () => { + const merged = mergeConfig({ url: '/x' }, { method: 'get' }); + assert.strictEqual(Object.getPrototypeOf(merged), null); + }); + + it('should preserve hasOwnProperty as a callable own slot', () => { + const merged = mergeConfig({}, { url: '/x', method: 'get' }); + assert.strictEqual(typeof merged.hasOwnProperty, 'function'); + assert.strictEqual(merged.hasOwnProperty('url'), true); + assert.strictEqual(merged.hasOwnProperty('method'), true); + assert.strictEqual(merged.hasOwnProperty('bogus'), false); + }); + + it('should not serialize hasOwnProperty slot via Object.keys', () => { + const merged = mergeConfig({ url: '/x' }, {}); + assert.ok(!Object.keys(merged).includes('hasOwnProperty')); + }); + + it('should not expose arbitrary polluted keys as inherited properties', () => { + Object.prototype.polluted = 'attacker'; + try { + const merged = mergeConfig({ url: '/x' }, {}); + assert.strictEqual(merged.polluted, undefined); + } finally { + delete Object.prototype.polluted; + } + }); + }); + + // Verify every gadget enumerated in the audit (extension of GHSA-q8qp-cvcw-x6jj) + // is neutralized end-to-end by the null-prototype config. + describe('Full gadget coverage via null-prototype config', () => { + function startEcho(handler) { + return new Promise((resolve) => { + const server = http.createServer(handler || ((req, res) => { + let body = ''; + req.on('data', (c) => (body += c)); + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + url: req.url, + method: req.method, + headers: req.headers, + body, + })); + }); + })); + server.listen(0, '127.0.0.1', () => resolve(server)); + }); + } + const stop = (s) => new Promise((r) => s.close(r)); + + it('should ignore polluted transformRequest', async () => { + let invoked = false; + Object.prototype.transformRequest = function polluted(data) { + invoked = true; + return 'INJECTED'; + }; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.post(`http://127.0.0.1:${port}/`, { hello: 'world' }); + assert.strictEqual(invoked, false); + assert.notStrictEqual(res.data.body, 'INJECTED'); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted transformResponse', async () => { + let invoked = false; + Object.prototype.transformResponse = function polluted() { + invoked = true; + return 'HIJACKED'; + }; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(invoked, false); + assert.notStrictEqual(res.data, 'HIJACKED'); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted adapter', async () => { + let hijacked = false; + Object.prototype.adapter = function pollutedAdapter() { + hijacked = true; + return Promise.resolve({ data: 'pwned', status: 200, statusText: 'OK', headers: {}, config: {}, request: {} }); + }; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/ok`); + assert.strictEqual(hijacked, false); + assert.notStrictEqual(res.data, 'pwned'); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted httpAgent', async () => { + let agentUsed = false; + Object.prototype.httpAgent = new http.Agent({ + keepAlive: false, + }); + // Wrap createConnection to detect usage + const origCreate = Object.prototype.httpAgent.createConnection; + Object.prototype.httpAgent.createConnection = function (...args) { + agentUsed = true; + return origCreate.apply(this, args); + }; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(res.status, 200); + assert.strictEqual(agentUsed, false); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted proxy', async () => { + Object.prototype.proxy = { + protocol: 'http', + host: '127.0.0.1', + port: 1, // would fail if actually used + }; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(res.status, 200); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted maxContentLength', async () => { + // Polluted tiny limit would reject a normal response if applied. + Object.prototype.maxContentLength = 1; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(res.status, 200); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted maxRedirects', async () => { + // Pollute with 0 — if picked up, follow-redirects path would be skipped. + // We make sure regular requests still succeed via the expected path. + Object.prototype.maxRedirects = 0; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(res.status, 200); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted timeout at the merged config level', () => { + Object.prototype.timeout = 1; + const merged = mergeConfig({}, { url: '/x' }); + assert.strictEqual(Object.prototype.hasOwnProperty.call(merged, 'timeout'), false); + assert.strictEqual(merged.timeout, undefined); + }); + + it('should ignore polluted timeoutErrorMessage', async () => { + Object.prototype.timeoutErrorMessage = 'INJECTED_TIMEOUT'; + // Not easy to assert without triggering a real timeout; just confirm + // normal requests still succeed and do not read the polluted key. + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(res.status, 200); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted transitional', async () => { + Object.prototype.transitional = { forcedJSONParsing: true, silentJSONParsing: false }; + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(res.status, 200); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted params and paramsSerializer', async () => { + let serializerInvoked = false; + Object.prototype.params = { injected: 'yes' }; + Object.prototype.paramsSerializer = function polluted() { + serializerInvoked = true; + return 'injected=yes'; + }; + + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/x`); + assert.strictEqual(serializerInvoked, false); + assert.strictEqual(res.data.url, '/x'); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted method', async () => { + Object.prototype.method = 'DELETE'; + const server = await startEcho(); + const { port } = server.address(); + try { + // axios.get should still send GET, not DELETE. + const res = await axios.get(`http://127.0.0.1:${port}/ok`); + assert.strictEqual(res.data.method, 'GET'); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted decompress', async () => { + Object.prototype.decompress = false; + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + assert.strictEqual(res.status, 200); + } finally { + await stop(server); + } + }, 10000); + + it('should ignore polluted responseType', async () => { + Object.prototype.responseType = 'arraybuffer'; + const server = await startEcho(); + const { port } = server.address(); + try { + const res = await axios.get(`http://127.0.0.1:${port}/`); + // When responseType is not set on config, json parsing should apply + // and res.data should be an object, not an ArrayBuffer/Buffer. + assert.strictEqual(typeof res.data, 'object'); + assert.ok(!Buffer.isBuffer(res.data)); + } finally { + await stop(server); + } + }, 10000); + }); });
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
6- github.com/axios/axios/commit/47915144662f2733e6c051bdcb895a8c8f0586aanvdPatchWEB
- github.com/axios/axios/pull/10779nvdIssue TrackingPatchWEB
- github.com/axios/axios/security/advisories/GHSA-q8qp-cvcw-x6jjnvdExploitMitigationVendor AdvisoryWEB
- github.com/advisories/GHSA-q8qp-cvcw-x6jjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-42264ghsaADVISORY
- github.com/axios/axios/releases/tag/v1.15.2nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.