mirror of
https://github.com/tenrok/axios.git
synced 2026-06-20 20:00:40 +03:00
fix: update the implementation to rather parse out the values than throw (#10687)
* fix: update the implementation to rather parse out the values than throw * fix: polynomal regex issues * fix: harden the security of proxy loopback
This commit is contained in:
+26
-27
@@ -5,41 +5,41 @@ import parseHeaders from '../helpers/parseHeaders.js';
|
|||||||
|
|
||||||
const $internals = Symbol('internals');
|
const $internals = Symbol('internals');
|
||||||
|
|
||||||
const isValidHeaderValue = (value) => !/[\r\n]/.test(value);
|
const INVALID_HEADER_VALUE_CHARS_RE = /[^\x09\x20-\x7E\x80-\xFF]/g;
|
||||||
|
|
||||||
function assertValidHeaderValue(value, header) {
|
function trimSPorHTAB(str) {
|
||||||
if (value === false || value == null) {
|
let start = 0;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.isArray(value)) {
|
|
||||||
value.forEach((v) => assertValidHeaderValue(v, header));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidHeaderValue(String(value))) {
|
|
||||||
throw new Error(`Invalid character in header content ["${header}"]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHeader(header) {
|
|
||||||
return header && String(header).trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripTrailingCRLF(str) {
|
|
||||||
let end = str.length;
|
let end = str.length;
|
||||||
|
|
||||||
while (end > 0) {
|
while (start < end) {
|
||||||
const charCode = str.charCodeAt(end - 1);
|
const code = str.charCodeAt(start);
|
||||||
|
|
||||||
if (charCode !== 10 && charCode !== 13) {
|
if (code !== 0x09 && code !== 0x20) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (end > start) {
|
||||||
|
const code = str.charCodeAt(end - 1);
|
||||||
|
|
||||||
|
if (code !== 0x09 && code !== 0x20) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
end -= 1;
|
end -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return end === str.length ? str : str.slice(0, end);
|
return start === 0 && end === str.length ? str : str.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeader(header) {
|
||||||
|
return header && String(header).trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeHeaderValue(str) {
|
||||||
|
return trimSPorHTAB(str.replace(INVALID_HEADER_VALUE_CHARS_RE, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeValue(value) {
|
function normalizeValue(value) {
|
||||||
@@ -47,7 +47,7 @@ function normalizeValue(value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.isArray(value) ? value.map(normalizeValue) : stripTrailingCRLF(String(value));
|
return utils.isArray(value) ? value.map(normalizeValue) : sanitizeHeaderValue(String(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTokens(str) {
|
function parseTokens(str) {
|
||||||
@@ -129,7 +129,6 @@ class AxiosHeaders {
|
|||||||
_rewrite === true ||
|
_rewrite === true ||
|
||||||
(_rewrite === undefined && self[key] !== false)
|
(_rewrite === undefined && self[key] !== false)
|
||||||
) {
|
) {
|
||||||
assertValidHeaderValue(_value, _header);
|
|
||||||
self[key || _header] = normalizeValue(_value);
|
self[key || _header] = normalizeValue(_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
const LOOPBACK_ADDRESSES = new Set(['localhost', '127.0.0.1', '::1']);
|
||||||
|
|
||||||
|
const isLoopback = (host) => LOOPBACK_ADDRESSES.has(host);
|
||||||
|
|
||||||
const DEFAULT_PORTS = {
|
const DEFAULT_PORTS = {
|
||||||
http: 80,
|
http: 80,
|
||||||
https: 443,
|
https: 443,
|
||||||
@@ -101,6 +105,6 @@ export default function shouldBypassProxy(location) {
|
|||||||
return hostname.endsWith(entryHost);
|
return hostname.endsWith(entryHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
return hostname === entryHost;
|
return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,15 +179,19 @@ describe('adapter (vitest browser)', () => {
|
|||||||
await responsePromise;
|
await responsePromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject request headers containing CRLF characters', async () => {
|
it('should sanitize request headers containing CRLF characters', async () => {
|
||||||
await expect(
|
const responsePromise = axios('/foo', {
|
||||||
axios('/foo', {
|
headers: {
|
||||||
headers: {
|
'x-test': '\tok\r\nInjected: yes ',
|
||||||
'x-test': 'ok\r\nInjected: yes',
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
).rejects.toThrow(/Invalid character in header content/);
|
|
||||||
|
|
||||||
expect(requests.length).toBe(0);
|
const request = await waitForRequest();
|
||||||
|
|
||||||
|
expect(request.requestHeaders['x-test']).toBe('okInjected: yes');
|
||||||
|
expect(request.requestHeaders.Injected).toBeUndefined();
|
||||||
|
|
||||||
|
request.respondWith();
|
||||||
|
await responsePromise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,15 +26,34 @@ const fetchAxios = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => {
|
describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => {
|
||||||
it('should reject request headers containing CRLF characters', async () => {
|
it('should sanitize request headers containing CRLF characters', async () => {
|
||||||
await assert.rejects(
|
const server = await startHTTPServer(
|
||||||
fetchAxios.get(`${LOCAL_SERVER_URL}/`, {
|
(req, res) => {
|
||||||
headers: {
|
res.setHeader('Content-Type', 'application/json');
|
||||||
'x-test': 'ok\r\nInjected: yes',
|
res.end(
|
||||||
},
|
JSON.stringify({
|
||||||
}),
|
xTest: req.headers['x-test'],
|
||||||
/(invalid.*header|header.*invalid)/i
|
injected: req.headers.injected ?? null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
port: SERVER_PORT,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await fetchAxios.get(`${LOCAL_SERVER_URL}/`, {
|
||||||
|
headers: {
|
||||||
|
'x-test': '\tok\r\nInjected: yes ',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(data.xTest, 'okInjected: yes');
|
||||||
|
assert.strictEqual(data.injected, null);
|
||||||
|
} finally {
|
||||||
|
await stopHTTPServer(server);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('responses', () => {
|
describe('responses', () => {
|
||||||
|
|||||||
@@ -126,15 +126,32 @@ describe('supports http with nodejs', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject request headers containing CRLF characters', async () => {
|
it('should sanitize request headers containing CRLF characters', async () => {
|
||||||
await assert.rejects(
|
const server = await startHTTPServer(
|
||||||
axios.get('http://localhost:1/', {
|
(req, res) => {
|
||||||
headers: {
|
res.setHeader('Content-Type', 'application/json');
|
||||||
'x-test': 'ok\r\nInjected: yes',
|
res.end(
|
||||||
},
|
JSON.stringify({
|
||||||
}),
|
xTest: req.headers['x-test'],
|
||||||
/Invalid character in header content/
|
injected: req.headers.injected ?? null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ port: SERVER_PORT }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`http://localhost:${server.address().port}/`, {
|
||||||
|
headers: {
|
||||||
|
'x-test': '\tok\r\nInjected: yes ',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(data.xTest, 'okInjected: yes');
|
||||||
|
assert.strictEqual(data.injected, null);
|
||||||
|
} finally {
|
||||||
|
await stopHTTPServer(server);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse the timeout property', async () => {
|
it('should parse the timeout property', async () => {
|
||||||
|
|||||||
@@ -103,20 +103,20 @@ describe('AxiosHeaders', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
it('should throw on CRLF in header value', () => {
|
it('should sanitize invalid characters in header value', () => {
|
||||||
const headers = new AxiosHeaders();
|
const headers = new AxiosHeaders();
|
||||||
|
|
||||||
assert.throws(() => {
|
headers.set('x-test', '\t safe\r\nInjected: true \u0000');
|
||||||
headers.set('x-test', 'safe\r\nInjected: true');
|
|
||||||
}, /Invalid character in header content/);
|
assert.strictEqual(headers.get('x-test'), 'safeInjected: true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on CRLF in any array header value', () => {
|
it('should sanitize invalid characters in any array header value', () => {
|
||||||
const headers = new AxiosHeaders();
|
const headers = new AxiosHeaders();
|
||||||
|
|
||||||
assert.throws(() => {
|
headers.set('set-cookie', ['safe=1', ' \tunsafe=1\nInjected: true\r\n ']);
|
||||||
headers.set('set-cookie', ['safe=1', 'unsafe=1\nInjected: true']);
|
|
||||||
}, /Invalid character in header content/);
|
assert.deepStrictEqual(headers.get('set-cookie'), ['safe=1', 'unsafe=1Injected: true']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,30 @@ describe('helpers::shouldBypassProxy', () => {
|
|||||||
expect(shouldBypassProxy('http://[::1]:8080/')).toBe(true);
|
expect(shouldBypassProxy('http://[::1]:8080/')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should bypass proxy for 127.0.0.1 when no_proxy contains localhost', () => {
|
||||||
|
setNoProxy('localhost');
|
||||||
|
|
||||||
|
expect(shouldBypassProxy('http://127.0.0.1:7777/')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bypass proxy for [::1] when no_proxy contains localhost', () => {
|
||||||
|
setNoProxy('localhost');
|
||||||
|
|
||||||
|
expect(shouldBypassProxy('http://[::1]:7777/')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bypass proxy for localhost when no_proxy contains 127.0.0.1', () => {
|
||||||
|
setNoProxy('127.0.0.1');
|
||||||
|
|
||||||
|
expect(shouldBypassProxy('http://localhost:7777/')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bypass proxy for localhost when no_proxy contains ::1', () => {
|
||||||
|
setNoProxy('::1');
|
||||||
|
|
||||||
|
expect(shouldBypassProxy('http://localhost:7777/')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should match wildcard and explicit ports', () => {
|
it('should match wildcard and explicit ports', () => {
|
||||||
setNoProxy('*.example.com,localhost:8080');
|
setNoProxy('*.example.com,localhost:8080');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user