mirror of
https://github.com/axios/axios.git
synced 2026-04-11 14:21:59 +08:00
fix: no_proxy hostname normalization bypass leads to ssrf (#10661)
This commit is contained in:
parent
8023035109
commit
fb3befb6da
@ -23,6 +23,7 @@ import formDataToStream from '../helpers/formDataToStream.js';
|
||||
import readBlob from '../helpers/readBlob.js';
|
||||
import ZlibHeaderTransformStream from '../helpers/ZlibHeaderTransformStream.js';
|
||||
import callbackify from '../helpers/callbackify.js';
|
||||
import shouldBypassProxy from '../helpers/shouldBypassProxy.js';
|
||||
import {
|
||||
progressEventReducer,
|
||||
progressEventDecorator,
|
||||
@ -192,7 +193,9 @@ function setProxy(options, configProxy, location) {
|
||||
if (!proxy && proxy !== false) {
|
||||
const proxyUrl = getProxyForUrl(location);
|
||||
if (proxyUrl) {
|
||||
proxy = new URL(proxyUrl);
|
||||
if (!shouldBypassProxy(location)) {
|
||||
proxy = new URL(proxyUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (proxy) {
|
||||
|
||||
106
lib/helpers/shouldBypassProxy.js
Normal file
106
lib/helpers/shouldBypassProxy.js
Normal file
@ -0,0 +1,106 @@
|
||||
const DEFAULT_PORTS = {
|
||||
http: 80,
|
||||
https: 443,
|
||||
ws: 80,
|
||||
wss: 443,
|
||||
ftp: 21,
|
||||
};
|
||||
|
||||
const parseNoProxyEntry = (entry) => {
|
||||
let entryHost = entry;
|
||||
let entryPort = 0;
|
||||
|
||||
if (entryHost.charAt(0) === '[') {
|
||||
const bracketIndex = entryHost.indexOf(']');
|
||||
|
||||
if (bracketIndex !== -1) {
|
||||
const host = entryHost.slice(1, bracketIndex);
|
||||
const rest = entryHost.slice(bracketIndex + 1);
|
||||
|
||||
if (rest.charAt(0) === ':' && /^\d+$/.test(rest.slice(1))) {
|
||||
entryPort = Number.parseInt(rest.slice(1), 10);
|
||||
}
|
||||
|
||||
return [host, entryPort];
|
||||
}
|
||||
}
|
||||
|
||||
const firstColon = entryHost.indexOf(':');
|
||||
const lastColon = entryHost.lastIndexOf(':');
|
||||
|
||||
if (
|
||||
firstColon !== -1 &&
|
||||
firstColon === lastColon &&
|
||||
/^\d+$/.test(entryHost.slice(lastColon + 1))
|
||||
) {
|
||||
entryPort = Number.parseInt(entryHost.slice(lastColon + 1), 10);
|
||||
entryHost = entryHost.slice(0, lastColon);
|
||||
}
|
||||
|
||||
return [entryHost, entryPort];
|
||||
};
|
||||
|
||||
const normalizeNoProxyHost = (hostname) => {
|
||||
if (!hostname) {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
|
||||
hostname = hostname.slice(1, -1);
|
||||
}
|
||||
|
||||
return hostname.replace(/\.+$/, '');
|
||||
};
|
||||
|
||||
export default function shouldBypassProxy(location) {
|
||||
let parsed;
|
||||
|
||||
try {
|
||||
parsed = new URL(location);
|
||||
} catch (_err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const noProxy = (process.env.no_proxy || process.env.NO_PROXY || '').toLowerCase();
|
||||
|
||||
if (!noProxy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (noProxy === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const port =
|
||||
Number.parseInt(parsed.port, 10) || DEFAULT_PORTS[parsed.protocol.split(':', 1)[0]] || 0;
|
||||
|
||||
const hostname = normalizeNoProxyHost(parsed.hostname.toLowerCase());
|
||||
|
||||
return noProxy.split(/[\s,]+/).some((entry) => {
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let [entryHost, entryPort] = parseNoProxyEntry(entry);
|
||||
|
||||
entryHost = normalizeNoProxyHost(entryHost);
|
||||
|
||||
if (!entryHost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entryPort && entryPort !== port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entryHost.charAt(0) === '*') {
|
||||
entryHost = entryHost.slice(1);
|
||||
}
|
||||
|
||||
if (entryHost.charAt(0) === '.') {
|
||||
return hostname.endsWith(entryHost);
|
||||
}
|
||||
|
||||
return hostname === entryHost;
|
||||
});
|
||||
}
|
||||
@ -1738,6 +1738,114 @@ describe('supports http with nodejs', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should not use proxy for localhost with trailing dot when listed in no_proxy', async () => {
|
||||
const originalHttpProxy = process.env.http_proxy;
|
||||
const originalHTTPProxy = process.env.HTTP_PROXY;
|
||||
const originalNoProxy = process.env.no_proxy;
|
||||
const originalNOProxy = process.env.NO_PROXY;
|
||||
|
||||
let proxyRequests = 0;
|
||||
const proxy = await startHTTPServer(
|
||||
(_, response) => {
|
||||
proxyRequests += 1;
|
||||
response.end('proxied');
|
||||
},
|
||||
{ port: PROXY_PORT }
|
||||
);
|
||||
|
||||
const noProxyValue = 'localhost,127.0.0.1,::1';
|
||||
const proxyUrl = `http://localhost:${proxy.address().port}/`;
|
||||
process.env.http_proxy = proxyUrl;
|
||||
process.env.HTTP_PROXY = proxyUrl;
|
||||
process.env.no_proxy = noProxyValue;
|
||||
process.env.NO_PROXY = noProxyValue;
|
||||
|
||||
try {
|
||||
await assert.rejects(axios.get('http://localhost.:1/', { timeout: 100 }));
|
||||
assert.equal(proxyRequests, 0, 'should not use proxy for localhost with trailing dot');
|
||||
} finally {
|
||||
await stopHTTPServer(proxy);
|
||||
|
||||
if (originalHttpProxy === undefined) {
|
||||
delete process.env.http_proxy;
|
||||
} else {
|
||||
process.env.http_proxy = originalHttpProxy;
|
||||
}
|
||||
|
||||
if (originalHTTPProxy === undefined) {
|
||||
delete process.env.HTTP_PROXY;
|
||||
} else {
|
||||
process.env.HTTP_PROXY = originalHTTPProxy;
|
||||
}
|
||||
|
||||
if (originalNoProxy === undefined) {
|
||||
delete process.env.no_proxy;
|
||||
} else {
|
||||
process.env.no_proxy = originalNoProxy;
|
||||
}
|
||||
|
||||
if (originalNOProxy === undefined) {
|
||||
delete process.env.NO_PROXY;
|
||||
} else {
|
||||
process.env.NO_PROXY = originalNOProxy;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should not use proxy for bracketed IPv6 loopback when listed in no_proxy', async () => {
|
||||
const originalHttpProxy = process.env.http_proxy;
|
||||
const originalHTTPProxy = process.env.HTTP_PROXY;
|
||||
const originalNoProxy = process.env.no_proxy;
|
||||
const originalNOProxy = process.env.NO_PROXY;
|
||||
|
||||
let proxyRequests = 0;
|
||||
const proxy = await startHTTPServer(
|
||||
(_, response) => {
|
||||
proxyRequests += 1;
|
||||
response.end('proxied');
|
||||
},
|
||||
{ port: PROXY_PORT }
|
||||
);
|
||||
|
||||
const noProxyValue = 'localhost,127.0.0.1,::1';
|
||||
const proxyUrl = `http://localhost:${proxy.address().port}/`;
|
||||
process.env.http_proxy = proxyUrl;
|
||||
process.env.HTTP_PROXY = proxyUrl;
|
||||
process.env.no_proxy = noProxyValue;
|
||||
process.env.NO_PROXY = noProxyValue;
|
||||
|
||||
try {
|
||||
await assert.rejects(axios.get('http://[::1]:1/', { timeout: 100 }));
|
||||
assert.equal(proxyRequests, 0, 'should not use proxy for IPv6 loopback');
|
||||
} finally {
|
||||
await stopHTTPServer(proxy);
|
||||
|
||||
if (originalHttpProxy === undefined) {
|
||||
delete process.env.http_proxy;
|
||||
} else {
|
||||
process.env.http_proxy = originalHttpProxy;
|
||||
}
|
||||
|
||||
if (originalHTTPProxy === undefined) {
|
||||
delete process.env.HTTP_PROXY;
|
||||
} else {
|
||||
process.env.HTTP_PROXY = originalHTTPProxy;
|
||||
}
|
||||
|
||||
if (originalNoProxy === undefined) {
|
||||
delete process.env.no_proxy;
|
||||
} else {
|
||||
process.env.no_proxy = originalNoProxy;
|
||||
}
|
||||
|
||||
if (originalNOProxy === undefined) {
|
||||
delete process.env.NO_PROXY;
|
||||
} else {
|
||||
process.env.NO_PROXY = originalNOProxy;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should use proxy for domains not in no_proxy', async () => {
|
||||
const originalHttpProxy = process.env.http_proxy;
|
||||
const originalHTTPProxy = process.env.HTTP_PROXY;
|
||||
|
||||
52
tests/unit/helpers/shouldBypassProxy.test.js
Normal file
52
tests/unit/helpers/shouldBypassProxy.test.js
Normal file
@ -0,0 +1,52 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import shouldBypassProxy from '../../../lib/helpers/shouldBypassProxy.js';
|
||||
|
||||
const originalNoProxy = process.env.no_proxy;
|
||||
const originalNOProxy = process.env.NO_PROXY;
|
||||
|
||||
const setNoProxy = (value) => {
|
||||
process.env.no_proxy = value;
|
||||
process.env.NO_PROXY = value;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (originalNoProxy === undefined) {
|
||||
delete process.env.no_proxy;
|
||||
} else {
|
||||
process.env.no_proxy = originalNoProxy;
|
||||
}
|
||||
|
||||
if (originalNOProxy === undefined) {
|
||||
delete process.env.NO_PROXY;
|
||||
} else {
|
||||
process.env.NO_PROXY = originalNOProxy;
|
||||
}
|
||||
});
|
||||
|
||||
describe('helpers::shouldBypassProxy', () => {
|
||||
it('should bypass proxy for localhost with a trailing dot', () => {
|
||||
setNoProxy('localhost,127.0.0.1,::1');
|
||||
|
||||
expect(shouldBypassProxy('http://localhost.:8080/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should bypass proxy for bracketed ipv6 loopback', () => {
|
||||
setNoProxy('localhost,127.0.0.1,::1');
|
||||
|
||||
expect(shouldBypassProxy('http://[::1]:8080/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support bracketed ipv6 entries in no_proxy', () => {
|
||||
setNoProxy('[::1]');
|
||||
|
||||
expect(shouldBypassProxy('http://[::1]:8080/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match wildcard and explicit ports', () => {
|
||||
setNoProxy('*.example.com,localhost:8080');
|
||||
|
||||
expect(shouldBypassProxy('http://api.example.com/')).toBe(true);
|
||||
expect(shouldBypassProxy('http://localhost:8080/')).toBe(true);
|
||||
expect(shouldBypassProxy('http://localhost:8081/')).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user