This commit is contained in:
Jay 2026-04-10 21:22:44 +02:00 committed by GitHub
commit c38e691c35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 393 additions and 4 deletions

View File

@ -19,6 +19,7 @@ var platform = require('../platform');
var fromDataURI = require('../helpers/fromDataURI');
var stream = require('stream');
var estimateDataURLDecodedBytes = require('../helpers/estimateDataURLDecodedBytes.js');
var shouldBypassProxy = require('../helpers/shouldBypassProxy');
var isHttps = /https:?/;
@ -46,9 +47,11 @@ function setProxy(options, configProxy, location) {
if (!proxy && proxy !== false) {
var proxyUrl = getProxyForUrl(location);
if (proxyUrl) {
proxy = url.parse(proxyUrl);
// replace 'host' since the proxy object is not a URL object
proxy.host = proxy.hostname;
if (!shouldBypassProxy(location)) {
proxy = url.parse(proxyUrl);
// replace 'host' since the proxy object is not a URL object
proxy.host = proxy.hostname;
}
}
}
if (proxy) {
@ -273,7 +276,7 @@ module.exports = function httpAdapter(config) {
} else {
options.hostname = parsed.hostname;
options.port = parsed.port;
setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
setProxy(options, config.proxy, protocol + '//' + parsed.host + options.path);
}
var transport;

View File

@ -6,6 +6,7 @@ var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var CanceledError = require('../cancel/CanceledError');
var normalizeHeaderName = require('../helpers/normalizeHeaderName');
var sanitizeHeaderValue = require('../helpers/sanitizeHeaderValue');
/**
* Throws a `CanceledError` if cancellation has been requested.
@ -58,6 +59,10 @@ module.exports = function dispatchRequest(config) {
}
);
utils.forEach(config.headers, function sanitizeHeaderConfigValue(value, header) {
config.headers[header] = sanitizeHeaderValue(value);
});
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {

View File

@ -0,0 +1,22 @@
'use strict';
var utils = require('../utils');
var INVALID_HEADER_VALUE_RE = /[^\x09\x20-\x7E\x80-\xFF]/g;
var BOUNDARY_WHITESPACE_RE = /^[\x09\x20]|[\x09\x20]$/g;
function sanitizeHeaderValue(value) {
if (value === false || value == null) {
return value;
}
if (utils.isArray(value)) {
return value.map(sanitizeHeaderValue);
}
return String(value)
.replace(INVALID_HEADER_VALUE_RE, '')
.replace(BOUNDARY_WHITESPACE_RE, '');
}
module.exports = sanitizeHeaderValue;

View File

@ -0,0 +1,129 @@
'use strict';
var URL = require('url').URL;
var DEFAULT_PORTS = {
http: 80,
https: 443,
ws: 80,
wss: 443,
ftp: 21
};
function parseNoProxyEntry(entry) {
var entryHost = entry;
var entryPort = 0;
if (entryHost.charAt(0) === '[') {
var bracketIndex = entryHost.indexOf(']');
if (bracketIndex !== -1) {
var host = entryHost.slice(1, bracketIndex);
var rest = entryHost.slice(bracketIndex + 1);
if (rest.charAt(0) === ':' && /^\d+$/.test(rest.slice(1))) {
entryPort = parseInt(rest.slice(1), 10);
}
return [host, entryPort];
}
}
var firstColon = entryHost.indexOf(':');
var lastColon = entryHost.lastIndexOf(':');
if (firstColon !== -1 && firstColon === lastColon && /^\d+$/.test(entryHost.slice(lastColon + 1))) {
entryPort = parseInt(entryHost.slice(lastColon + 1), 10);
entryHost = entryHost.slice(0, lastColon);
}
return [entryHost, entryPort];
}
function normalizeNoProxyHost(hostname) {
if (!hostname) {
return hostname;
}
if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
hostname = hostname.slice(1, -1);
}
return hostname.replace(/\.+$/, '');
}
function isLoopbackIPv4(hostname) {
var octets = hostname.split('.');
if (octets.length !== 4) {
return false;
}
if (octets[0] !== '127') {
return false;
}
return octets.every(function testOctet(octet) {
return /^\d+$/.test(octet) && Number(octet) >= 0 && Number(octet) <= 255;
});
}
function isLoopbackHost(hostname) {
return hostname === 'localhost' || hostname === '::1' || isLoopbackIPv4(hostname);
}
module.exports = function shouldBypassProxy(location) {
var parsed;
try {
parsed = new URL(location);
} catch (err) {
return false;
}
var noProxy = (process.env.no_proxy || process.env.NO_PROXY || '').toLowerCase();
if (!noProxy) {
return false;
}
if (noProxy === '*') {
return true;
}
var protocol = parsed.protocol.split(':', 1)[0];
var port = parseInt(parsed.port, 10) || DEFAULT_PORTS[protocol] || 0;
var hostname = normalizeNoProxyHost(parsed.hostname.toLowerCase());
return noProxy.split(/[\s,]+/).some(function testNoProxyEntry(entry) {
if (!entry) {
return false;
}
var entryParts = parseNoProxyEntry(entry);
var entryHost = normalizeNoProxyHost(entryParts[0]);
var entryPort = entryParts[1];
if (!entryHost) {
return false;
}
if (entryPort && entryPort !== port) {
return false;
}
if (isLoopbackHost(hostname) && isLoopbackHost(entryHost)) {
return true;
}
if (entryHost.charAt(0) === '*') {
entryHost = entryHost.slice(1);
}
if (entryHost.charAt(0) === '.') {
return hostname.slice(-entryHost.length) === entryHost;
}
return hostname === entryHost;
});
};

View File

@ -112,4 +112,17 @@ describe('headers', function () {
done();
});
});
it('should sanitize headers containing invalid characters', function (done) {
axios('/foo', {
headers: {
'x-test': ' ok\r\nInjected: yes\t'
}
});
getAjaxRequest().then(function (request) {
testHeaderValue(request.requestHeaders, 'x-test', 'okInjected: yes');
done();
});
});
});

View File

@ -31,8 +31,39 @@ describe('supports http with nodejs', function () {
proxy = null;
}
delete process.env.http_proxy;
delete process.env.HTTP_PROXY;
delete process.env.https_proxy;
delete process.env.no_proxy;
delete process.env.NO_PROXY;
});
it('should sanitize request headers containing invalid characters', function (done) {
server = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'text/plain');
res.end(req.headers['x-test']);
}).listen(4444, function () {
axios.get('http://localhost:4444/', {
headers: {
'x-test': ' ok\r\nInjected: yes\t'
}
}).then(function (response) {
assert.equal(response.data, 'okInjected: yes');
done();
}).catch(done);
});
});
it('should preserve request error for unavailable host with invalid characters', function (done) {
axios.get('http://localhost:1/', {
headers: {
'x-test': 'ok\r\nInjected: yes'
}
}).then(function () {
done(new Error('request should not succeed'));
}).catch(function (error) {
assert.notEqual(error.message, 'Invalid character in header content ["x-test"]');
done();
});
});
it('should throw an error if the timeout property is not parsable as a number', function (done) {
@ -967,6 +998,98 @@ describe('supports http with nodejs', function () {
});
});
it('should not use proxy for localhost with trailing dot when listed in no_proxy', function (done) {
var proxyRequests = 0;
proxy = http.createServer(function (request, response) {
proxyRequests += 1;
response.end('proxied');
}).listen(4000, function () {
process.env.http_proxy = 'http://localhost:4000/';
process.env.HTTP_PROXY = 'http://localhost:4000/';
process.env.no_proxy = 'localhost,127.0.0.1,::1';
process.env.NO_PROXY = 'localhost,127.0.0.1,::1';
axios.get('http://localhost.:1/', {
timeout: 100
}).then(function () {
done(new Error('request should not succeed'));
}).catch(function () {
assert.equal(proxyRequests, 0, 'should not use proxy for localhost with trailing dot');
done();
});
});
});
it('should not use proxy for bracketed IPv6 loopback when listed in no_proxy', function (done) {
var proxyRequests = 0;
proxy = http.createServer(function (request, response) {
proxyRequests += 1;
response.end('proxied');
}).listen(4000, function () {
process.env.http_proxy = 'http://localhost:4000/';
process.env.HTTP_PROXY = 'http://localhost:4000/';
process.env.no_proxy = 'localhost,127.0.0.1,::1';
process.env.NO_PROXY = 'localhost,127.0.0.1,::1';
axios.get('http://[::1]:1/', {
timeout: 100
}).then(function () {
done(new Error('request should not succeed'));
}).catch(function () {
assert.equal(proxyRequests, 0, 'should not use proxy for IPv6 loopback');
done();
});
});
});
it('should not use proxy for 127.0.0.1 when no_proxy is localhost', function (done) {
var proxyRequests = 0;
proxy = http.createServer(function (request, response) {
proxyRequests += 1;
response.end('proxied');
}).listen(4000, function () {
process.env.http_proxy = 'http://localhost:4000/';
process.env.HTTP_PROXY = 'http://localhost:4000/';
process.env.no_proxy = 'localhost';
process.env.NO_PROXY = 'localhost';
axios.get('http://127.0.0.1:1/', {
timeout: 100
}).then(function () {
done(new Error('request should not succeed'));
}).catch(function () {
assert.equal(proxyRequests, 0, 'should not use proxy for IPv4 loopback alias');
done();
});
});
});
it('should not use proxy for [::1] when no_proxy is localhost', function (done) {
var proxyRequests = 0;
proxy = http.createServer(function (request, response) {
proxyRequests += 1;
response.end('proxied');
}).listen(4000, function () {
process.env.http_proxy = 'http://localhost:4000/';
process.env.HTTP_PROXY = 'http://localhost:4000/';
process.env.no_proxy = 'localhost';
process.env.NO_PROXY = 'localhost';
axios.get('http://[::1]:1/', {
timeout: 100
}).then(function () {
done(new Error('request should not succeed'));
}).catch(function () {
assert.equal(proxyRequests, 0, 'should not use proxy for IPv6 loopback alias');
done();
});
});
});
it('should use proxy for domains not in no_proxy', function (done) {
server = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'text/html; charset=UTF-8');

View File

@ -0,0 +1,20 @@
var assert = require('assert');
var sanitizeHeaderValue = require('../../../lib/helpers/sanitizeHeaderValue');
describe('helpers::sanitizeHeaderValue', function () {
it('should remove invalid header characters', function () {
assert.strictEqual(sanitizeHeaderValue('ok\r\nInjected: yes'), 'okInjected: yes');
assert.strictEqual(sanitizeHeaderValue('ok\x01bad'), 'okbad');
});
it('should remove boundary whitespace', function () {
assert.strictEqual(sanitizeHeaderValue(' value\t'), 'value');
});
it('should sanitize array values recursively', function () {
assert.deepStrictEqual(
sanitizeHeaderValue([' safe=1 ', 'unsafe=1\nInjected: true']),
['safe=1', 'unsafe=1Injected: true']
);
});
});

View File

@ -0,0 +1,74 @@
var assert = require('assert');
var shouldBypassProxy = require('../../../lib/helpers/shouldBypassProxy');
var originalNoProxy = process.env.no_proxy;
var originalNOProxy = process.env.NO_PROXY;
function setNoProxy(value) {
process.env.no_proxy = value;
process.env.NO_PROXY = value;
}
describe('helpers::shouldBypassProxy', function () {
afterEach(function () {
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 bypass proxy for localhost with a trailing dot', function () {
setNoProxy('localhost,127.0.0.1,::1');
assert.strictEqual(shouldBypassProxy('http://localhost.:8080/'), true);
});
it('should bypass proxy for bracketed ipv6 loopback', function () {
setNoProxy('localhost,127.0.0.1,::1');
assert.strictEqual(shouldBypassProxy('http://[::1]:8080/'), true);
});
it('should support bracketed ipv6 entries in no_proxy', function () {
setNoProxy('[::1]');
assert.strictEqual(shouldBypassProxy('http://[::1]:8080/'), true);
});
it('should match wildcard and explicit ports', function () {
setNoProxy('*.example.com,localhost:8080');
assert.strictEqual(shouldBypassProxy('http://api.example.com/'), true);
assert.strictEqual(shouldBypassProxy('http://localhost:8080/'), true);
assert.strictEqual(shouldBypassProxy('http://localhost:8081/'), false);
});
it('should treat localhost and loopback IP aliases as equivalent', function () {
setNoProxy('localhost');
assert.strictEqual(shouldBypassProxy('http://127.0.0.1:8080/'), true);
assert.strictEqual(shouldBypassProxy('http://[::1]:8080/'), true);
setNoProxy('127.0.0.1');
assert.strictEqual(shouldBypassProxy('http://localhost:8080/'), true);
assert.strictEqual(shouldBypassProxy('http://[::1]:8080/'), true);
setNoProxy('::1');
assert.strictEqual(shouldBypassProxy('http://localhost:8080/'), true);
assert.strictEqual(shouldBypassProxy('http://127.0.0.1:8080/'), true);
});
it('should keep loopback alias matching port-aware', function () {
setNoProxy('localhost:8080');
assert.strictEqual(shouldBypassProxy('http://127.0.0.1:8080/'), true);
assert.strictEqual(shouldBypassProxy('http://[::1]:8080/'), true);
assert.strictEqual(shouldBypassProxy('http://127.0.0.1:8081/'), false);
});
});