diff --git a/README.md b/README.md index d0af2fe0..6c002bdc 100644 --- a/README.md +++ b/README.md @@ -452,6 +452,9 @@ These are the available config options for making requests. Only the `url` is re // `xsrfHeaderName` is the name of the http header that carries the xsrf token value xsrfHeaderName: 'X-XSRF-TOKEN', // default + + // `undefined` (default) - set XSRF header only for the same origin requests + withXSRFToken: boolean | undefined | ((config: InternalAxiosRequestConfig) => boolean | undefined), // `onUploadProgress` allows handling of progress events for uploads // browser & node.js diff --git a/index.d.cts b/index.d.cts index 77214a9a..35a1f0fe 100644 --- a/index.d.cts +++ b/index.d.cts @@ -414,6 +414,7 @@ declare namespace axios { family?: AddressFamily; lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: LookupAddress | LookupAddress[], family?: AddressFamily) => void) => void) | ((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>); + withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined); } // Alias diff --git a/index.d.ts b/index.d.ts index 38786132..3a2840bb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -355,6 +355,7 @@ export interface AxiosRequestConfig { family?: AddressFamily; lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: LookupAddress | LookupAddress[], family?: AddressFamily) => void) => void) | ((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>); + withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined); } // Alias diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index 92f0daf2..26126b2f 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -49,7 +49,7 @@ export default isXHRAdapterSupported && function (config) { return new Promise(function dispatchXhrRequest(resolve, reject) { let requestData = config.data; const requestHeaders = AxiosHeaders.from(config.headers).normalize(); - const responseType = config.responseType; + let {responseType, withXSRFToken} = config; let onCanceled; function done() { if (config.cancelToken) { @@ -185,13 +185,16 @@ export default isXHRAdapterSupported && function (config) { // Add xsrf header // This is only done if running in a standard browser environment. // Specifically not if we're in a web worker, or react-native. - if (platform.hasStandardBrowserEnv) { - // Add xsrf header - // regarding CVE-2023-45857 config.withCredentials condition was removed temporarily - const xsrfValue = isURLSameOrigin(fullPath) && config.xsrfCookieName && cookies.read(config.xsrfCookieName); + if(platform.hasStandardBrowserEnv) { + withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(config)); - if (xsrfValue) { - requestHeaders.set(config.xsrfHeaderName, xsrfValue); + if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(fullPath))) { + // Add xsrf header + const xsrfValue = config.xsrfHeaderName && config.xsrfCookieName && cookies.read(config.xsrfCookieName); + + if (xsrfValue) { + requestHeaders.set(config.xsrfHeaderName, xsrfValue); + } } } diff --git a/lib/core/mergeConfig.js b/lib/core/mergeConfig.js index 671e3345..bbbf96d4 100644 --- a/lib/core/mergeConfig.js +++ b/lib/core/mergeConfig.js @@ -75,6 +75,7 @@ export default function mergeConfig(config1, config2) { timeout: defaultToConfig2, timeoutMessage: defaultToConfig2, withCredentials: defaultToConfig2, + withXSRFToken: defaultToConfig2, adapter: defaultToConfig2, responseType: defaultToConfig2, xsrfCookieName: defaultToConfig2, diff --git a/lib/helpers/cookies.js b/lib/helpers/cookies.js index 2a052003..d039ac4f 100644 --- a/lib/helpers/cookies.js +++ b/lib/helpers/cookies.js @@ -1,52 +1,42 @@ -'use strict'; - import utils from './../utils.js'; import platform from '../platform/index.js'; export default platform.hasStandardBrowserEnv ? -// Standard browser envs support document.cookie - (function standardBrowserEnv() { - return { - write: function write(name, value, expires, path, domain, secure) { - const cookie = []; - cookie.push(name + '=' + encodeURIComponent(value)); + // Standard browser envs support document.cookie + { + write(name, value, expires, path, domain, secure) { + const cookie = [name + '=' + encodeURIComponent(value)]; - if (utils.isNumber(expires)) { - cookie.push('expires=' + new Date(expires).toGMTString()); - } + utils.isNumber(expires) && cookie.push('expires=' + new Date(expires).toGMTString()); - if (utils.isString(path)) { - cookie.push('path=' + path); - } + utils.isString(path) && cookie.push('path=' + path); - if (utils.isString(domain)) { - cookie.push('domain=' + domain); - } + utils.isString(domain) && cookie.push('domain=' + domain); - if (secure === true) { - cookie.push('secure'); - } + secure === true && cookie.push('secure'); - document.cookie = cookie.join('; '); - }, + document.cookie = cookie.join('; '); + }, - read: function read(name) { - const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); - return (match ? decodeURIComponent(match[3]) : null); - }, + read(name) { + const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); + return (match ? decodeURIComponent(match[3]) : null); + }, - remove: function remove(name) { - this.write(name, '', Date.now() - 86400000); - } - }; - })() : + remove(name) { + this.write(name, '', Date.now() - 86400000); + } + } + + : + + // Non-standard browser env (web workers, react-native) lack needed support. + { + write() {}, + read() { + return null; + }, + remove() {} + }; -// Non standard browser env (web workers, react-native) lack needed support. - (function nonStandardBrowserEnv() { - return { - write: function write() {}, - read: function read() { return null; }, - remove: function remove() {} - }; - })(); diff --git a/lib/helpers/isURLSameOrigin.js b/lib/helpers/isURLSameOrigin.js index cab47d9c..a8678a4e 100644 --- a/lib/helpers/isURLSameOrigin.js +++ b/lib/helpers/isURLSameOrigin.js @@ -13,7 +13,7 @@ export default platform.hasStandardBrowserEnv ? let originURL; /** - * Parse a URL to discover it's components + * Parse a URL to discover its components * * @param {String} url The URL to be parsed * @returns {Object} diff --git a/test/specs/xsrf.spec.js b/test/specs/xsrf.spec.js index add5afc5..b05f70e3 100644 --- a/test/specs/xsrf.spec.js +++ b/test/specs/xsrf.spec.js @@ -79,4 +79,68 @@ describe('xsrf', function () { done(); }); }); + + describe('withXSRFToken option', function(){ + + it('should set xsrf header for cross origin when withXSRFToken = true', function (done) { + const token = '12345'; + + document.cookie = axios.defaults.xsrfCookieName + '=' + token; + + axios('http://example.com/', { + withXSRFToken: true + }); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual(token); + done(); + }); + }); + + it('should not set xsrf header for the same origin when withXSRFToken = false', function (done) { + const token = '12345'; + + document.cookie = axios.defaults.xsrfCookieName + '=' + token; + + axios('/foo', { + withXSRFToken: false + }); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual(undefined); + done(); + }); + }); + + it('should not set xsrf header for the same origin when withXSRFToken = false', function (done) { + const token = '12345'; + + document.cookie = axios.defaults.xsrfCookieName + '=' + token; + + axios('/foo', { + withXSRFToken: false + }); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual(undefined); + done(); + }); + }); + + it('should support function resolver', (done) => { + const token = '12345'; + + document.cookie = axios.defaults.xsrfCookieName + '=' + token; + + axios('/foo', { + withXSRFToken: (config) => config.userFlag === 'yes', + userFlag: 'yes' + }); + + getAjaxRequest().then(function (request) { + expect(request.requestHeaders[axios.defaults.xsrfHeaderName]).toEqual(token); + done(); + }); + }); + }); });