2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00

fix(http): preserve TLS options for proxy tunnels (#10957)

This commit is contained in:
Jay
2026-05-28 20:46:22 +02:00
committed by GitHub
parent a2390a5c05
commit 3dec28f94c
3 changed files with 80 additions and 2 deletions
+1
View File
@@ -13,6 +13,7 @@
- **Types:** Add the missing readonly `name: 'CanceledError'` declaration to CommonJS `CanceledError` typings to match the ESM declarations. (**#10922**)
- **Types:** Correct the CommonJS `isCancel` type guard to narrow cancellation errors to `CanceledError<T>`, matching the ESM declaration. (**#10952**)
- **HTTP Adapter - Auth on Redirect:** HTTP Basic credentials supplied via `config.auth` are now restored on same-origin redirects, fixing a regression caused by `follow-redirects` >= 1.15.8 that broke `POST` requests answered with a 303 Location. Cross-origin redirects continue to drop credentials, preserving the existing T-R2 mitigation in `THREATMODEL.md`. (**#6929**)
- **HTTP Adapter - Proxy TLS:** Preserve `httpsAgent` TLS options such as `ca` and `rejectUnauthorized` for HTTPS origins reached through a CONNECT proxy tunnel. (**#10953**)
- **HTTP Adapter - Socket Path:** Ignore inherited `socketPath` and `allowedSocketPaths` config values when building Node.js requests, preventing prototype-pollution SSRF via Unix sockets. (**#10901**)
- **React Native FormData:** Clear the default `Content-Type` header for React Native `FormData` requests so Android can build multipart bodies with the correct boundary. (**#10898**)
- **Request Data:** Preserve enumerable symbol keys when merging plain request data before `transformRequest`. (**#6392**)
+8
View File
@@ -112,6 +112,14 @@ function getTunnelingAgent(agentOptions, userHttpsAgent) {
? { ...userHttpsAgent.options, ...agentOptions }
: agentOptions;
agent = new HttpsProxyAgent(merged);
if (userHttpsAgent && userHttpsAgent.options) {
const originTLSOptions = { ...userHttpsAgent.options };
const callback = agent.callback;
agent.callback = function axiosTunnelingAgentCallback(req, opts) {
// HttpsProxyAgent v5 reads callback opts for the post-CONNECT origin TLS upgrade.
return callback.call(this, req, { ...originTLSOptions, ...opts });
};
}
agent[kAxiosInstalledTunnel] = true;
cache.set(key, agent);
return agent;
+71 -2
View File
@@ -2015,6 +2015,75 @@ describe('supports http with nodejs', () => {
}
});
it('should apply httpsAgent TLS options to CONNECT-tunneled origins (issue #10953)', async () => {
const tlsOptions = {
key: fs.readFileSync(path.join(adaptersTestsDir, 'key.pem')),
cert: fs.readFileSync(path.join(adaptersTestsDir, 'cert.pem')),
};
const origin = await new Promise((resolve, reject) => {
const s = https.createServer(tlsOptions, (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
res.end('trusted-through-agent');
});
s.listen(0, 'localhost', () => resolve(s));
s.on('error', reject);
});
const captured = { plaintext: 0, connectTargets: [] };
const upstreamSockets = [];
const proxy = await new Promise((resolve, reject) => {
const p = http.createServer(() => {
captured.plaintext += 1;
});
p.on('connect', (req, clientSocket, head) => {
captured.connectTargets.push(req.url);
const [host, port] = req.url.split(':');
const upstream = net.connect(Number(port), host, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
if (head && head.length) upstream.write(head);
upstream.pipe(clientSocket);
clientSocket.pipe(upstream);
});
upstreamSockets.push(upstream);
upstream.on('error', () => clientSocket.destroy());
clientSocket.on('error', () => upstream.destroy());
});
p.listen(0, '127.0.0.1', () => resolve(p));
p.on('error', reject);
});
const httpsAgent = new https.Agent({ ca: tlsOptions.cert });
try {
const response = await axios.get(`https://localhost:${origin.address().port}/`, {
httpsAgent,
proxy: {
host: '127.0.0.1',
port: proxy.address().port,
protocol: 'http',
},
});
assert.strictEqual(response.data, 'trusted-through-agent');
assert.strictEqual(captured.plaintext, 0, 'proxy must not see plaintext HTTPS requests');
assert.strictEqual(captured.connectTargets.length, 1, 'proxy should see exactly one CONNECT');
assert.ok(
captured.connectTargets[0].startsWith(`localhost:${origin.address().port}`),
`CONNECT should target the origin host:port, got ${captured.connectTargets[0]}`
);
} finally {
httpsAgent.destroy();
for (const s of upstreamSockets) s.destroy();
origin.closeAllConnections?.();
proxy.closeAllConnections?.();
origin.close();
proxy.close();
origin.unref?.();
proxy.unref?.();
}
});
it('should surface a CONNECT 407 from the proxy as an AxiosError (issue #6320)', async () => {
const proxy = await new Promise((resolve, reject) => {
const p = http.createServer();
@@ -3369,11 +3438,11 @@ describe('supports http with nodejs', () => {
assert.ok(options.agent instanceof HttpsProxyAgent);
});
it('forwards user httpsAgent options to the tunneling agent so origin TLS uses them', () => {
it('includes user httpsAgent options in the tunneling agent constructor options', () => {
const userAgent = new https.Agent({ rejectUnauthorized: false, ca: 'sentinel-ca' });
const options = buildOptions();
__setProxy(options, proxyConfig, 'https://example.com/', false, userAgent);
// HttpsProxyAgent v5 surfaces the merged constructor options on `.proxy`.
// Origin TLS behavior is covered by the issue #10953 integration test.
assert.strictEqual(options.agent.proxy.rejectUnauthorized, false);
assert.strictEqual(options.agent.proxy.ca, 'sentinel-ca');
});