diff --git a/lib/core/AxiosHeaders.js b/lib/core/AxiosHeaders.js index 08e5ccf5..f57e98f8 100644 --- a/lib/core/AxiosHeaders.js +++ b/lib/core/AxiosHeaders.js @@ -5,41 +5,41 @@ import parseHeaders from '../helpers/parseHeaders.js'; 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) { - if (value === false || value == null) { - 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) { +function trimSPorHTAB(str) { + let start = 0; let end = str.length; - while (end > 0) { - const charCode = str.charCodeAt(end - 1); + while (start < end) { + 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; } 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) { @@ -47,7 +47,7 @@ function normalizeValue(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) { @@ -129,7 +129,6 @@ class AxiosHeaders { _rewrite === true || (_rewrite === undefined && self[key] !== false) ) { - assertValidHeaderValue(_value, _header); self[key || _header] = normalizeValue(_value); } } diff --git a/lib/helpers/shouldBypassProxy.js b/lib/helpers/shouldBypassProxy.js index 2460695d..d90902c4 100644 --- a/lib/helpers/shouldBypassProxy.js +++ b/lib/helpers/shouldBypassProxy.js @@ -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 = { http: 80, https: 443, @@ -101,6 +105,6 @@ export default function shouldBypassProxy(location) { return hostname.endsWith(entryHost); } - return hostname === entryHost; + return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost)); }); } diff --git a/tests/browser/adapter.browser.test.js b/tests/browser/adapter.browser.test.js index 33128318..fd3f5903 100644 --- a/tests/browser/adapter.browser.test.js +++ b/tests/browser/adapter.browser.test.js @@ -179,15 +179,19 @@ describe('adapter (vitest browser)', () => { await responsePromise; }); - it('should reject request headers containing CRLF characters', async () => { - await expect( - axios('/foo', { - headers: { - 'x-test': 'ok\r\nInjected: yes', - }, - }) - ).rejects.toThrow(/Invalid character in header content/); + it('should sanitize request headers containing CRLF characters', async () => { + const responsePromise = axios('/foo', { + headers: { + 'x-test': '\tok\r\nInjected: yes ', + }, + }); - 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; }); }); diff --git a/tests/unit/adapters/fetch.test.js b/tests/unit/adapters/fetch.test.js index 0be0ab57..f90a0b03 100644 --- a/tests/unit/adapters/fetch.test.js +++ b/tests/unit/adapters/fetch.test.js @@ -26,15 +26,34 @@ const fetchAxios = axios.create({ }); describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => { - it('should reject request headers containing CRLF characters', async () => { - await assert.rejects( - fetchAxios.get(`${LOCAL_SERVER_URL}/`, { - headers: { - 'x-test': 'ok\r\nInjected: yes', - }, - }), - /(invalid.*header|header.*invalid)/i + it('should sanitize request headers containing CRLF characters', async () => { + const server = await startHTTPServer( + (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.end( + JSON.stringify({ + xTest: req.headers['x-test'], + 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', () => { diff --git a/tests/unit/adapters/http.test.js b/tests/unit/adapters/http.test.js index 96e4ea7e..edf4d96f 100644 --- a/tests/unit/adapters/http.test.js +++ b/tests/unit/adapters/http.test.js @@ -126,15 +126,32 @@ describe('supports http with nodejs', () => { } }); - it('should reject request headers containing CRLF characters', async () => { - await assert.rejects( - axios.get('http://localhost:1/', { - headers: { - 'x-test': 'ok\r\nInjected: yes', - }, - }), - /Invalid character in header content/ + it('should sanitize request headers containing CRLF characters', async () => { + const server = await startHTTPServer( + (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.end( + JSON.stringify({ + xTest: req.headers['x-test'], + 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 () => { diff --git a/tests/unit/axiosHeaders.test.js b/tests/unit/axiosHeaders.test.js index 60b23b5d..7cbb77d5 100644 --- a/tests/unit/axiosHeaders.test.js +++ b/tests/unit/axiosHeaders.test.js @@ -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(); - assert.throws(() => { - headers.set('x-test', 'safe\r\nInjected: true'); - }, /Invalid character in header content/); + headers.set('x-test', '\t safe\r\nInjected: true \u0000'); + + 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(); - assert.throws(() => { - headers.set('set-cookie', ['safe=1', 'unsafe=1\nInjected: true']); - }, /Invalid character in header content/); + headers.set('set-cookie', ['safe=1', ' \tunsafe=1\nInjected: true\r\n ']); + + assert.deepStrictEqual(headers.get('set-cookie'), ['safe=1', 'unsafe=1Injected: true']); }); }); diff --git a/tests/unit/helpers/shouldBypassProxy.test.js b/tests/unit/helpers/shouldBypassProxy.test.js index 0879cceb..6e63143a 100644 --- a/tests/unit/helpers/shouldBypassProxy.test.js +++ b/tests/unit/helpers/shouldBypassProxy.test.js @@ -42,6 +42,30 @@ describe('helpers::shouldBypassProxy', () => { 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', () => { setNoProxy('*.example.com,localhost:8080');