diff --git a/lib/helpers/resolveConfig.js b/lib/helpers/resolveConfig.js index bf22d591..24610670 100644 --- a/lib/helpers/resolveConfig.js +++ b/lib/helpers/resolveConfig.js @@ -54,10 +54,18 @@ export default (config) => { // Specifically not if we're in a web worker, or react-native. if (platform.hasStandardBrowserEnv) { - withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(newConfig)); + if (utils.isFunction(withXSRFToken)) { + withXSRFToken = withXSRFToken(newConfig); + } - if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(newConfig.url))) { - // Add xsrf header + // Strict boolean check — prevents proto-pollution gadgets (e.g. Object.prototype.withXSRFToken = 1) + // and misconfigurations (e.g. "false") from short-circuiting the same-origin check and leaking + // the XSRF token cross-origin. See GHSA-xx6v-rp6x-q39c. + const shouldSendXSRF = + withXSRFToken === true || + (withXSRFToken == null && isURLSameOrigin(newConfig.url)); + + if (shouldSendXSRF) { const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName); if (xsrfValue) { diff --git a/tests/browser/xsrf.browser.test.js b/tests/browser/xsrf.browser.test.js index 027ab83b..ac2170ff 100644 --- a/tests/browser/xsrf.browser.test.js +++ b/tests/browser/xsrf.browser.test.js @@ -179,4 +179,60 @@ describe('xsrf (vitest browser)', () => { expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBe(token); }); }); + + // GHSA-xx6v-rp6x-q39c: non-boolean truthy withXSRFToken must not short-circuit + // the same-origin check and leak the XSRF token cross-origin. + describe('GHSA-xx6v-rp6x-q39c non-boolean withXSRFToken', () => { + afterEach(() => { + delete Object.prototype.withXSRFToken; + }); + + const leakCases = [ + ['number 1', 1], + ['string "false"', 'false'], + ['empty object', {}], + ['empty array', []], + ]; + + leakCases.forEach(([label, value]) => { + it(`should not send xsrf header cross-origin when withXSRFToken = ${label}`, async () => { + setXsrfCookie('12345'); + + const request = await sendRequest('http://example.com/', { + withXSRFToken: value, + }); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBeUndefined(); + }); + }); + + it('should not send xsrf header cross-origin when Object.prototype.withXSRFToken is polluted', async () => { + Object.prototype.withXSRFToken = 1; + setXsrfCookie('12345'); + + const request = await sendRequest('http://example.com/'); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBeUndefined(); + }); + + it('should still send xsrf header cross-origin when withXSRFToken === true (strict)', async () => { + const token = '12345'; + setXsrfCookie(token); + + const request = await sendRequest('http://example.com/', { + withXSRFToken: true, + }); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBe(token); + }); + + it('should still send xsrf header same-origin when withXSRFToken is undefined', async () => { + const token = '12345'; + setXsrfCookie(token); + + const request = await sendRequest('/foo'); + + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toBe(token); + }); + }); });