Skip to content

Commit 8a23d7e

Browse files
fix: prevent open redirect via protocol-relative returnTo
1 parent 361c018 commit 8a23d7e

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

__tests__/with-authentication-required.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,33 @@ describe('withAuthenticationRequired', () => {
182182
);
183183
});
184184

185+
it('should sanitize protocol-relative returnTo paths to prevent open redirect', async () => {
186+
// Simulate a URL like https://app.example.com//evil.com whose pathname is //evil.com.
187+
// Routers (react-router, next.js, gatsby) treat //evil.com as a protocol-relative URL
188+
// and redirect the user to http://evil.com.
189+
window.history.replaceState({}, '', 'https://www.example.com//evil.com');
190+
191+
mockClient.getUser.mockResolvedValue(undefined);
192+
const MyComponent = () => <>Private</>;
193+
const WrappedComponent = withAuthenticationRequired(MyComponent);
194+
render(
195+
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
196+
<WrappedComponent />
197+
</Auth0Provider>
198+
);
199+
await waitFor(() =>
200+
expect(mockClient.loginWithRedirect).toHaveBeenCalledWith(
201+
expect.objectContaining({
202+
appState: expect.objectContaining({
203+
returnTo: '/evil.com',
204+
}),
205+
})
206+
)
207+
);
208+
209+
window.history.replaceState({}, '', 'https://www.example.com/');
210+
});
211+
185212
it('should call loginWithRedirect only once even if parent state changes', async () => {
186213
mockClient.getUser.mockResolvedValue(undefined);
187214
const MyComponent = () => <>Private</>;

src/with-authentication-required.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ const defaultOnBeforeAuthentication = async (): Promise<void> => {/* noop */ };
1818
/**
1919
* @ignore
2020
*/
21-
const defaultReturnTo = (): string =>
22-
`${window.location.pathname}${window.location.search}`;
21+
const defaultReturnTo = (): string => {
22+
// Normalize the pathname to prevent protocol-relative open redirects.
23+
// A URL like https://app.example.com//evil.com produces a pathname of
24+
// //evil.com, which routers (react-router, next.js, gatsby) interpret as a
25+
// protocol-relative URL and redirect the user to an external host.
26+
const pathname = window.location.pathname.replace(/^\/\/+/, '/');
27+
return `${pathname}${window.location.search}`;
28+
};
2329

2430
/**
2531
* Options for the withAuthenticationRequired Higher Order Component

0 commit comments

Comments
 (0)