Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions __tests__/with-authentication-required.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,36 @@ describe('withAuthenticationRequired', () => {
);
});

it('should sanitize protocol-relative returnTo paths to prevent open redirect', async () => {
// Simulate a URL like https://app.example.com//evil.com whose pathname is //evil.com.
// Routers (react-router, next.js, gatsby) treat //evil.com as a protocol-relative URL
// and redirect the user to http://evil.com.
const originalUrl = window.location.href;
window.history.replaceState({}, '', 'https://www.example.com//evil.com');

try {
mockClient.getUser.mockResolvedValue(undefined);
const MyComponent = () => <>Private</>;
const WrappedComponent = withAuthenticationRequired(MyComponent);
render(
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
<WrappedComponent />
</Auth0Provider>
);
await waitFor(() =>
expect(mockClient.loginWithRedirect).toHaveBeenCalledWith(
expect.objectContaining({
appState: expect.objectContaining({
returnTo: '/evil.com',
}),
})
)
);
} finally {
window.history.replaceState({}, '', originalUrl);
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('should call loginWithRedirect only once even if parent state changes', async () => {
mockClient.getUser.mockResolvedValue(undefined);
const MyComponent = () => <>Private</>;
Expand Down
10 changes: 8 additions & 2 deletions src/with-authentication-required.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ const defaultOnBeforeAuthentication = async (): Promise<void> => {/* noop */ };
/**
* @ignore
*/
const defaultReturnTo = (): string =>
`${window.location.pathname}${window.location.search}`;
const defaultReturnTo = (): string => {
// Normalize the pathname to prevent protocol-relative open redirects.
// A URL like https://app.example.com//evil.com produces a pathname of
// //evil.com, which routers (react-router, next.js, gatsby) interpret as a
// protocol-relative URL and redirect the user to an external host.
const pathname = window.location.pathname.replace(/^\/\/+/, '/');
return `${pathname}${window.location.search}`;
};

/**
* Options for the withAuthenticationRequired Higher Order Component
Expand Down
Loading