From 471952cb4117464f3f3cea1ecf2e49cd1ece6f3d Mon Sep 17 00:00:00 2001 From: Gopi-bruno Date: Fri, 26 Jun 2026 13:15:28 +0530 Subject: [PATCH 1/2] feat: add forwardAuthorizationOnRedirect setting to allow stripping sensitive headers on cross-origin redirects --- .../components/RequestPane/Settings/index.js | 21 ++- .../components/Sidebar/NewRequest/index.js | 2 +- .../ReduxStore/slices/collections/actions.js | 3 +- .../ReduxStore/slices/collections/index.js | 4 + .../src/runner/run-single-request.js | 4 + .../bruno-cli/src/utils/axios-instance.js | 22 +++ .../tests/utils/axios-instance.spec.js | 134 ++++++++++++++++++ .../src/opencollection/items/graphql.ts | 6 +- .../src/opencollection/items/http.ts | 6 +- .../src/postman/postman-to-bruno.js | 3 +- .../src/ipc/network/axios-instance.js | 30 +++- .../bruno-electron/src/ipc/network/index.js | 6 +- .../tests/network/axios-instance.spec.js | 134 ++++++++++++++++++ packages/bruno-lang/v2/src/bruToJson.js | 8 ++ .../bruno-schema-types/src/collection/item.ts | 1 + .../bruno-schema/src/collections/index.js | 1 + packages/bruno-tests/src/redirect/index.js | 7 + .../cross-origin-redirect-auth-forward.bru | 23 +++ .../cross-origin-redirect-auth-strip.bru | 23 +++ .../settings/redirect-auth-strip.spec.ts | 55 +++++++ 20 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 tests/request/settings/collection/cross-origin-redirect-auth-forward.bru create mode 100644 tests/request/settings/collection/cross-origin-redirect-auth-strip.bru create mode 100644 tests/request/settings/redirect-auth-strip.spec.ts diff --git a/packages/bruno-app/src/components/RequestPane/Settings/index.js b/packages/bruno-app/src/components/RequestPane/Settings/index.js index 12df747b1e6..a0066872604 100644 --- a/packages/bruno-app/src/components/RequestPane/Settings/index.js +++ b/packages/bruno-app/src/components/RequestPane/Settings/index.js @@ -14,7 +14,8 @@ const DEFAULT_SETTINGS = { encodeUrl: false, followRedirects: true, maxRedirects: 5, - timeout: 'inherit' + timeout: 'inherit', + forwardAuthorizationOnRedirect: true }; const Settings = ({ item, collection }) => { @@ -26,7 +27,7 @@ const Settings = ({ item, collection }) => { const rawSettings = getPropertyFromDraftOrRequest('settings'); const settings = { ...DEFAULT_SETTINGS, ...rawSettings }; - const { encodeUrl, followRedirects, maxRedirects, timeout } = settings; + const { encodeUrl, followRedirects, maxRedirects, timeout, forwardAuthorizationOnRedirect } = settings; // Reusable function to update settings const updateSetting = useCallback((settingUpdate) => { @@ -45,6 +46,9 @@ const Settings = ({ item, collection }) => { const onToggleFollowRedirects = useCallback(() => updateSetting({ followRedirects: !followRedirects }), [followRedirects, updateSetting]); + const onToggleForwardAuthorizationOnRedirect = useCallback(() => + updateSetting({ forwardAuthorizationOnRedirect: !forwardAuthorizationOnRedirect }), [forwardAuthorizationOnRedirect, updateSetting]); + const onMaxRedirectsChange = useCallback((e) => { const value = e.target.value; // Only allow empty string or digits @@ -107,7 +111,7 @@ const Settings = ({ item, collection }) => { -
+
{ />
+
+ +
+ { .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request')); } else if (values.requestType === 'from-curl') { const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected); - const settings = { encodeUrl: false }; + const settings = { encodeUrl: false, forwardAuthorizationOnRedirect: false }; dispatch( newHttpRequest({ diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index db0da635309..9dc4c704e3c 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1401,7 +1401,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { } }, settings: settings ?? { - encodeUrl: true + encodeUrl: true, + forwardAuthorizationOnRedirect: false } }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 0818b5e2613..4b6bafeb669 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -963,6 +963,10 @@ export const collectionsSlice = createSlice({ content: null } }, + settings: { + encodeUrl: true, + forwardAuthorizationOnRedirect: false + }, draft: null }; item.draft = cloneDeep(item); diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 433430a52fe..dc17402ae7f 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -514,6 +514,9 @@ const runSingleRequest = async function ( // Get followRedirects setting, default to true for backward compatibility const followRedirects = request.settings?.followRedirects ?? true; + // Get forwardAuthorizationOnRedirect setting, default to true for backward compatibility + const forwardAuthorizationOnRedirect = request.settings?.forwardAuthorizationOnRedirect ?? true; + // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5 let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5; @@ -620,6 +623,7 @@ const runSingleRequest = async function ( requestMaxRedirects: requestMaxRedirects, disableCookies: options.disableCookies, followRedirects: followRedirects, + forwardAuthorizationOnRedirect: forwardAuthorizationOnRedirect, proxyMode, proxyConfig, systemProxyConfig: cachedSystemProxy, diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js index 01796ea98ce..be4a654031b 100644 --- a/packages/bruno-cli/src/utils/axios-instance.js +++ b/packages/bruno-cli/src/utils/axios-instance.js @@ -66,6 +66,16 @@ const createRedirectConfig = (error, redirectUrl) => { return requestConfig; }; +const isSameOrigin = (url1, url2) => { + try { + const parsed1 = new global.URL(url1); + const parsed2 = new global.URL(url2); + return parsed1.origin === parsed2.origin; + } catch (err) { + return false; + } +}; + /** * Function that configures axios with timing interceptors * Important to note here that the timings are not completely accurate. @@ -76,6 +86,7 @@ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies, followRedirects = true, + forwardAuthorizationOnRedirect = true, proxyMode, proxyConfig, systemProxyConfig, @@ -179,6 +190,17 @@ function makeAxiosInstance({ const requestConfig = createRedirectConfig(error, redirectUrl); + if (!forwardAuthorizationOnRedirect) { + if (!isSameOrigin(error.config.url, redirectUrl)) { + Object.keys(requestConfig.headers).forEach((key) => { + const lowerKey = key.toLowerCase(); + if (lowerKey === 'authorization' || lowerKey === 'proxy-authorization') { + delete requestConfig.headers[key]; + } + }); + } + } + await setupProxyAgents({ requestConfig, proxyMode, diff --git a/packages/bruno-cli/tests/utils/axios-instance.spec.js b/packages/bruno-cli/tests/utils/axios-instance.spec.js index 1e25f317fa3..6445ffe2974 100644 --- a/packages/bruno-cli/tests/utils/axios-instance.spec.js +++ b/packages/bruno-cli/tests/utils/axios-instance.spec.js @@ -34,4 +34,138 @@ describe('makeAxiosInstance', () => { expect(stubAdapter.getConfig().headers['User-Agent']).toMatch(/^bruno-runtime\//); }); + + describe('cross-origin redirects authorization stripping', () => { + function createRedirectingStubAdapter(redirectUrl, redirectStatus = 302) { + const calls = []; + const adapter = (config) => { + calls.push(config); + if (calls.length === 1) { + const err = new Error('Redirect ' + redirectStatus); + err.config = config; + err.response = { + status: redirectStatus, + statusText: 'Found', + headers: { + location: redirectUrl + }, + data: {} + }; + return Promise.reject(err); + } + return Promise.resolve({ + data: { success: true }, + status: 200, + statusText: 'OK', + headers: {}, + config + }); + }; + adapter.getCalls = () => calls; + return adapter; + } + + it('should strip Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationOnRedirect is false', async () => { + const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: false + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'Authorization': 'Bearer my-token', + 'Proxy-Authorization': 'Bearer proxy-token', + 'Custom-Header': 'keep-me' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + + // First call should have headers + expect(calls[0].headers['Authorization']).toBe('Bearer my-token'); + expect(calls[0].headers['Proxy-Authorization']).toBe('Bearer proxy-token'); + expect(calls[0].headers['Custom-Header']).toBe('keep-me'); + + // Redirected call should strip auth headers but keep custom headers + expect(calls[1].headers['Authorization']).toBeUndefined(); + expect(calls[1].headers['Proxy-Authorization']).toBeUndefined(); + expect(calls[1].headers['Custom-Header']).toBe('keep-me'); + }); + + it('should preserve Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationOnRedirect is true', async () => { + const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: true + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'authorization': 'Bearer my-token', + 'proxy-authorization': 'Bearer proxy-token', + 'Custom-Header': 'keep-me' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + expect(calls[1].headers['authorization']).toBe('Bearer my-token'); + expect(calls[1].headers['proxy-authorization']).toBe('Bearer proxy-token'); + expect(calls[1].headers['Custom-Header']).toBe('keep-me'); + }); + + it('should preserve Authorization and Proxy-Authorization headers on same-origin redirect even if forwardAuthorizationOnRedirect is false', async () => { + const stubAdapter = createRedirectingStubAdapter('https://api.example.com/target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: false + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'Authorization': 'Bearer my-token', + 'Proxy-Authorization': 'Bearer proxy-token' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + expect(calls[1].headers['Authorization']).toBe('Bearer my-token'); + expect(calls[1].headers['Proxy-Authorization']).toBe('Bearer proxy-token'); + }); + + it('should preserve Authorization and Proxy-Authorization headers on relative redirect even if forwardAuthorizationOnRedirect is false', async () => { + const stubAdapter = createRedirectingStubAdapter('/relative-target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: false + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'Authorization': 'Bearer my-token', + 'Proxy-Authorization': 'Bearer proxy-token' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + expect(calls[1].headers['Authorization']).toBe('Bearer my-token'); + expect(calls[1].headers['Proxy-Authorization']).toBe('Bearer proxy-token'); + }); + }); }); diff --git a/packages/bruno-converters/src/opencollection/items/graphql.ts b/packages/bruno-converters/src/opencollection/items/graphql.ts index caac82de570..42fad6415ad 100644 --- a/packages/bruno-converters/src/opencollection/items/graphql.ts +++ b/packages/bruno-converters/src/opencollection/items/graphql.ts @@ -90,6 +90,9 @@ export const fromOpenCollectionGraphqlItem = (item: GraphQLRequest): BrunoItem = if (settings.maxRedirects !== undefined) { (brunoItem.settings as Record).maxRedirects = settings.maxRedirects; } + if (settings.forwardAuthorizationOnRedirect !== undefined) { + (brunoItem.settings as Record).forwardAuthorizationOnRedirect = settings.forwardAuthorizationOnRedirect; + } } if (info.tags?.length) { @@ -181,7 +184,8 @@ export const toOpenCollectionGraphqlItem = (item: BrunoItem): GraphQLRequest => encodeUrl: typeof brunoSettings.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true, timeout: typeof brunoSettings.timeout === 'number' ? brunoSettings.timeout : 0, followRedirects: typeof brunoSettings.followRedirects === 'boolean' ? brunoSettings.followRedirects : true, - maxRedirects: typeof brunoSettings.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5 + maxRedirects: typeof brunoSettings.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5, + forwardAuthorizationOnRedirect: typeof brunoSettings.forwardAuthorizationOnRedirect === 'boolean' ? brunoSettings.forwardAuthorizationOnRedirect : false }; ocRequest.settings = settings; diff --git a/packages/bruno-converters/src/opencollection/items/http.ts b/packages/bruno-converters/src/opencollection/items/http.ts index 8efca076d57..a260b0a3a97 100644 --- a/packages/bruno-converters/src/opencollection/items/http.ts +++ b/packages/bruno-converters/src/opencollection/items/http.ts @@ -107,7 +107,8 @@ export const fromOpenCollectionHttpItem = (ocRequest: HttpRequest): BrunoItem => encodeUrl: typeof ocRequest.settings.encodeUrl === 'boolean' ? ocRequest.settings.encodeUrl : true, timeout: typeof ocRequest.settings.timeout === 'number' ? ocRequest.settings.timeout : 0, followRedirects: typeof ocRequest.settings.followRedirects === 'boolean' ? ocRequest.settings.followRedirects : true, - maxRedirects: typeof ocRequest.settings.maxRedirects === 'number' ? ocRequest.settings.maxRedirects : 5 + maxRedirects: typeof ocRequest.settings.maxRedirects === 'number' ? ocRequest.settings.maxRedirects : 5, + forwardAuthorizationOnRedirect: typeof ocRequest.settings.forwardAuthorizationOnRedirect === 'boolean' ? ocRequest.settings.forwardAuthorizationOnRedirect : false }; brunoItem.settings = settings; } @@ -223,7 +224,8 @@ export const toOpenCollectionHttpItem = (item: BrunoItem): HttpRequest => { encodeUrl: typeof brunoSettings?.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true, timeout: typeof brunoSettings?.timeout === 'number' ? brunoSettings.timeout : 0, followRedirects: typeof brunoSettings?.followRedirects === 'boolean' ? brunoSettings.followRedirects : true, - maxRedirects: typeof brunoSettings?.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5 + maxRedirects: typeof brunoSettings?.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5, + forwardAuthorizationOnRedirect: typeof brunoSettings?.forwardAuthorizationOnRedirect === 'boolean' ? brunoSettings.forwardAuthorizationOnRedirect : false }; ocRequest.settings = settings; diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index 8027c181bfb..6796b91e752 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -506,7 +506,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } }; const settings = { - encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true + encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true, + forwardAuthorizationOnRedirect: false }; // Handle followRedirects setting diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js index 19b48119054..4cae396c1cd 100644 --- a/packages/bruno-electron/src/ipc/network/axios-instance.js +++ b/packages/bruno-electron/src/ipc/network/axios-instance.js @@ -71,6 +71,16 @@ const checkConnection = (host, port) => * @see https://github.com/axios/axios/issues/695 * @returns {axios.AxiosInstance} */ +const isSameOrigin = (url1, url2) => { + try { + const parsed1 = new global.URL(url1); + const parsed2 = new global.URL(url2); + return parsed1.origin === parsed2.origin; + } catch (err) { + return false; + } +}; + function makeAxiosInstance({ proxyMode = 'off', proxyModeReason = '', @@ -78,7 +88,8 @@ function makeAxiosInstance({ requestMaxRedirects = 5, httpsAgentRequestFields = {}, interpolationOptions = {}, - followRedirects = true + followRedirects = true, + forwardAuthorizationOnRedirect = true } = {}) { /** @type {axios.AxiosInstance} */ const instance = axios.create({ @@ -354,6 +365,23 @@ function makeAxiosInstance({ } }; + if (!forwardAuthorizationOnRedirect) { + if (!isSameOrigin(error.config.url, redirectUrl)) { + Object.keys(requestConfig.headers).forEach((key) => { + const lowerKey = key.toLowerCase(); + if (lowerKey === 'authorization' || lowerKey === 'proxy-authorization') { + delete requestConfig.headers[key]; + } + }); + + timeline.push({ + timestamp: new Date(), + type: 'info', + message: `Cross-origin redirect: stripping Authorization and Proxy-Authorization headers` + }); + } + } + // Apply proper HTTP redirect behavior based on status code const statusCode = error.response.status; const originalMethod = (error.config.method || 'get').toLowerCase(); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 5bef312e889..7c3ee3438dd 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -134,6 +134,9 @@ const configureRequest = async ( // Get followRedirects setting, default to true for backward compatibility const followRedirects = request.settings?.followRedirects ?? true; + // Get forwardAuthorizationOnRedirect setting, default to true for backward compatibility + const forwardAuthorizationOnRedirect = request.settings?.forwardAuthorizationOnRedirect ?? true; + // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5 let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5; @@ -158,7 +161,8 @@ const configureRequest = async ( requestMaxRedirects, httpsAgentRequestFields, interpolationOptions, - followRedirects + followRedirects, + forwardAuthorizationOnRedirect }); if (request.ntlmConfig) { diff --git a/packages/bruno-electron/tests/network/axios-instance.spec.js b/packages/bruno-electron/tests/network/axios-instance.spec.js index 407ff102c8e..a5ccdf1f527 100644 --- a/packages/bruno-electron/tests/network/axios-instance.spec.js +++ b/packages/bruno-electron/tests/network/axios-instance.spec.js @@ -207,3 +207,137 @@ describe('axios-instance: DNS lookup behavior (GitHub #7343)', () => { expect(config.lookup).not.toBe(inheritedLookup); }); }); + +describe('axios-instance: cross-origin redirects authorization stripping', () => { + function createRedirectingStubAdapter(redirectUrl, redirectStatus = 302) { + const calls = []; + const adapter = (config) => { + calls.push(config); + if (calls.length === 1) { + const err = new Error('Redirect ' + redirectStatus); + err.config = config; + err.response = { + status: redirectStatus, + statusText: 'Found', + headers: { + location: redirectUrl + }, + data: {} + }; + return Promise.reject(err); + } + return Promise.resolve({ + data: { success: true }, + status: 200, + statusText: 'OK', + headers: {}, + config + }); + }; + adapter.getCalls = () => calls; + return adapter; + } + + test('should strip Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationOnRedirect is false', async () => { + const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: false + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'Authorization': 'Bearer my-token', + 'Proxy-Authorization': 'Bearer proxy-token', + 'Custom-Header': 'keep-me' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + + // First call should have headers + expect(calls[0].headers['Authorization']).toBe('Bearer my-token'); + expect(calls[0].headers['Proxy-Authorization']).toBe('Bearer proxy-token'); + expect(calls[0].headers['Custom-Header']).toBe('keep-me'); + + // Redirected call should strip auth headers but keep custom headers + expect(calls[1].headers['Authorization']).toBeUndefined(); + expect(calls[1].headers['Proxy-Authorization']).toBeUndefined(); + expect(calls[1].headers['Custom-Header']).toBe('keep-me'); + }); + + test('should preserve Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationOnRedirect is true', async () => { + const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: true + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'authorization': 'Bearer my-token', + 'proxy-authorization': 'Bearer proxy-token', + 'Custom-Header': 'keep-me' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + expect(calls[1].headers['authorization']).toBe('Bearer my-token'); + expect(calls[1].headers['proxy-authorization']).toBe('Bearer proxy-token'); + expect(calls[1].headers['Custom-Header']).toBe('keep-me'); + }); + + test('should preserve Authorization and Proxy-Authorization headers on same-origin redirect even if forwardAuthorizationOnRedirect is false', async () => { + const stubAdapter = createRedirectingStubAdapter('https://api.example.com/target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: false + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'Authorization': 'Bearer my-token', + 'Proxy-Authorization': 'Bearer proxy-token' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + expect(calls[1].headers['Authorization']).toBe('Bearer my-token'); + expect(calls[1].headers['Proxy-Authorization']).toBe('Bearer proxy-token'); + }); + + test('should preserve Authorization and Proxy-Authorization headers on relative redirect even if forwardAuthorizationOnRedirect is false', async () => { + const stubAdapter = createRedirectingStubAdapter('/relative-target'); + const instance = makeAxiosInstance({ + followRedirects: true, + forwardAuthorizationOnRedirect: false + }); + + await instance({ + url: 'https://api.example.com/start', + method: 'get', + headers: { + 'Authorization': 'Bearer my-token', + 'Proxy-Authorization': 'Bearer proxy-token' + }, + adapter: stubAdapter + }); + + const calls = stubAdapter.getCalls(); + expect(calls.length).toBe(2); + expect(calls[1].headers['Authorization']).toBe('Bearer my-token'); + expect(calls[1].headers['Proxy-Authorization']).toBe('Bearer proxy-token'); + }); +}); diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index f62ad973738..be8481b34db 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -542,6 +542,10 @@ const sem = grammar.createSemantics().addAttribute('ast', { parsedSettings.followRedirects = typeof settings.followRedirects === 'boolean' ? settings.followRedirects : settings.followRedirects === 'true'; } + if (settings.forwardAuthorizationOnRedirect !== undefined) { + parsedSettings.forwardAuthorizationOnRedirect = typeof settings.forwardAuthorizationOnRedirect === 'boolean' ? settings.forwardAuthorizationOnRedirect : settings.forwardAuthorizationOnRedirect === 'true'; + } + // Parse maxRedirects as number if (settings.maxRedirects !== undefined) { const maxRedirects = parseInt(settings.maxRedirects, 10); @@ -575,6 +579,10 @@ const sem = grammar.createSemantics().addAttribute('ast', { _settings.maxRedirects = parsedSettings.maxRedirects; } + if (parsedSettings.forwardAuthorizationOnRedirect !== undefined) { + _settings.forwardAuthorizationOnRedirect = parsedSettings.forwardAuthorizationOnRedirect; + } + if (keepAliveInterval) { _settings.keepAliveInterval = keepAliveInterval; } diff --git a/packages/bruno-schema-types/src/collection/item.ts b/packages/bruno-schema-types/src/collection/item.ts index 00aa1dabf43..621f32abac6 100644 --- a/packages/bruno-schema-types/src/collection/item.ts +++ b/packages/bruno-schema-types/src/collection/item.ts @@ -16,6 +16,7 @@ export interface HttpItemSettings { followRedirects?: boolean | null; maxRedirects?: number | null; timeout?: number | 'inherit' | null; + forwardAuthorizationOnRedirect?: boolean | null; } export interface WebSocketItemSettings { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index a997c96a423..f27912a175b 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -661,6 +661,7 @@ const itemSchema = Yup.object({ followRedirects: Yup.boolean().nullable(), maxRedirects: Yup.number().min(0).max(50).nullable(), timeout: Yup.mixed().nullable(), + forwardAuthorizationOnRedirect: Yup.boolean().nullable(), }).noUnknown(true) .strict() .nullable() diff --git a/packages/bruno-tests/src/redirect/index.js b/packages/bruno-tests/src/redirect/index.js index 7fdf6b52681..414de383194 100644 --- a/packages/bruno-tests/src/redirect/index.js +++ b/packages/bruno-tests/src/redirect/index.js @@ -103,6 +103,13 @@ router.get('/anything', function (req, res) { }); }); +router.get('/cross-origin', function (req, res) { + const host = req.headers.host || 'localhost:8081'; + const targetHost = host.includes('127.0.0.1') ? host.replace('127.0.0.1', 'localhost') : host.replace('localhost', '127.0.0.1'); + const protocol = req.secure ? 'https' : 'http'; + res.status(302).set('Location', `${protocol}://${targetHost}/api/redirect/anything`).send('Redirecting cross-origin'); +}); + router.get('/:count', function (req, res) { const count = parseInt(req.params.count, 10); diff --git a/tests/request/settings/collection/cross-origin-redirect-auth-forward.bru b/tests/request/settings/collection/cross-origin-redirect-auth-forward.bru new file mode 100644 index 00000000000..b19a502fc4a --- /dev/null +++ b/tests/request/settings/collection/cross-origin-redirect-auth-forward.bru @@ -0,0 +1,23 @@ +meta { + name: cross-origin-redirect-auth-forward + type: http + seq: 5 +} + +get { + url: http://127.0.0.1:8081/api/redirect/cross-origin + body: none + auth: none +} + +headers { + Authorization: Bearer token-test + Proxy-Authorization: Bearer proxy-test +} + +settings { + followRedirects: true + forwardAuthorizationOnRedirect: true + maxRedirects: 5 + timeout: 0 +} diff --git a/tests/request/settings/collection/cross-origin-redirect-auth-strip.bru b/tests/request/settings/collection/cross-origin-redirect-auth-strip.bru new file mode 100644 index 00000000000..4807e02994a --- /dev/null +++ b/tests/request/settings/collection/cross-origin-redirect-auth-strip.bru @@ -0,0 +1,23 @@ +meta { + name: cross-origin-redirect-auth-strip + type: http + seq: 4 +} + +get { + url: http://127.0.0.1:8081/api/redirect/cross-origin + body: none + auth: none +} + +headers { + Authorization: Bearer token-test + Proxy-Authorization: Bearer proxy-test +} + +settings { + followRedirects: true + forwardAuthorizationOnRedirect: false + maxRedirects: 5 + timeout: 0 +} diff --git a/tests/request/settings/redirect-auth-strip.spec.ts b/tests/request/settings/redirect-auth-strip.spec.ts new file mode 100644 index 00000000000..75aafcd38f3 --- /dev/null +++ b/tests/request/settings/redirect-auth-strip.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Redirect Authorization Stripping E2E Tests', () => { + test('should strip Authorization and Proxy-Authorization on cross-origin redirects when setting is OFF', async ({ + pageWithUserData: page + }) => { + // Open collection + await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible(); + await page.locator('#sidebar-collection-name').getByText('settings-test').click(); + + // Open request + await page.getByRole('complementary').getByText('cross-origin-redirect-auth-strip').click(); + + // Send request + await page.getByTestId('send-arrow-icon').click(); + + // Verify status code + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + + // Verify headers are stripped + const responseTexts = await page.getByTestId('response-preview-container').locator('.CodeMirror-scroll').allInnerTexts(); + const fullText = responseTexts.join('\n'); + expect(fullText).not.toContain('"authorization":'); + expect(fullText).not.toContain('"proxy-authorization":'); + + // Close tab + await page.locator('.close-icon-container').click({ force: true }); + }); + + test('should preserve Authorization and Proxy-Authorization on cross-origin redirects when setting is ON', async ({ + pageWithUserData: page + }) => { + // Open collection + await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible(); + await page.locator('#sidebar-collection-name').getByText('settings-test').click(); + + // Open request + await page.getByRole('complementary').getByText('cross-origin-redirect-auth-forward').click(); + + // Send request + await page.getByTestId('send-arrow-icon').click(); + + // Verify status code + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + + // Verify headers are preserved + const responseTexts = await page.getByTestId('response-preview-container').locator('.CodeMirror-scroll').allInnerTexts(); + const fullText = responseTexts.join('\n'); + expect(fullText).toContain('"authorization": "Bearer token-test"'); + expect(fullText).toContain('"proxy-authorization": "Bearer proxy-test"'); + + // Close tab + await page.locator('.close-icon-container').click({ force: true }); + }); +}); From a1fc25ddc6471d947d64142bc8dfabc2ba7b5756 Mon Sep 17 00:00:00 2001 From: Gopi-bruno Date: Fri, 26 Jun 2026 13:16:58 +0530 Subject: [PATCH 2/2] feat: add forwardAuthorizationOnRedirect to opencollection types --- .../bruno-converters/src/opencollection/types.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/bruno-converters/src/opencollection/types.ts b/packages/bruno-converters/src/opencollection/types.ts index 0be53a34c53..ddfbf7e0a90 100644 --- a/packages/bruno-converters/src/opencollection/types.ts +++ b/packages/bruno-converters/src/opencollection/types.ts @@ -228,4 +228,16 @@ export interface BrunoCollectionRoot { name?: string; seq?: number; }; +} +// FIX ME (draft) - need to update this in @opencollection/types package +declare module '@opencollection/types/requests/graphql' { + export interface GraphQLRequestSettings { + forwardAuthorizationOnRedirect?: boolean | 'inherit'; + } +} + +declare module '@opencollection/types/requests/http' { + export interface HttpRequestSettings { + forwardAuthorizationOnRedirect?: boolean | 'inherit'; + } } \ No newline at end of file