-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Feat/handling authorization header leaks to third party servers on redirect #8380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
471952c
a1fc25d
e0f95b5
c5c45d8
e919f87
55856c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ import toast from 'react-hot-toast'; | |
| import mime from 'mime-types'; | ||
| import path from 'utils/common/path'; | ||
| import { getUniqueTagsFromItems } from 'utils/collections/index'; | ||
| import { DEFAULT_HTTP_ITEM_SETTINGS } from 'utils/common/constants'; | ||
| import { getCollectionEnvironmentPath } from 'utils/snapshot'; | ||
| import { getDataTypeFromValue } from '@usebruno/common/utils'; | ||
| import * as exampleReducers from './exampleReducers'; | ||
|
|
@@ -963,6 +964,7 @@ export const collectionsSlice = createSlice({ | |
| content: null | ||
| } | ||
| }, | ||
| settings: cloneDeep(DEFAULT_HTTP_ITEM_SETTINGS), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need cloneDeep here? DEFAULT_HTTP_ITEM_SETTINGS is lain flat object with only primitive values. |
||
| draft: null | ||
| }; | ||
| item.draft = cloneDeep(item); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 forwardAuthorizationHeader is false', async () => { | ||
| const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target'); | ||
| const instance = makeAxiosInstance({ | ||
| followRedirects: true, | ||
| forwardAuthorizationHeader: 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'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also add expect(calls[1].url).toBe('https://other-domain.com/target'); |
||
| }); | ||
|
|
||
| it('should preserve Authorization and Proxy-Authorization headers on cross-origin redirect when forwardAuthorizationHeader is true', async () => { | ||
| const stubAdapter = createRedirectingStubAdapter('https://other-domain.com/target'); | ||
| const instance = makeAxiosInstance({ | ||
| followRedirects: true, | ||
| forwardAuthorizationHeader: 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 forwardAuthorizationHeader is false', async () => { | ||
| const stubAdapter = createRedirectingStubAdapter('https://api.example.com/target'); | ||
| const instance = makeAxiosInstance({ | ||
| followRedirects: true, | ||
| forwardAuthorizationHeader: 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 forwardAuthorizationHeader is false', async () => { | ||
| const stubAdapter = createRedirectingStubAdapter('/relative-target'); | ||
| const instance = makeAxiosInstance({ | ||
| followRedirects: true, | ||
| forwardAuthorizationHeader: 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'); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we please change the name to something like forwardAuthorizationOnRedirect or shouldForwardAuthorizationHeader?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Earlier it was named
forwardAuthorizationOnRedirectbut after discussing with Bijin and Anoop,forwardAuthorizationHeaderwas finalised yesterday.