fix(#337): Sanitize OAuth callback error parameter to prevent open redirect
- Validate error against allowlist of OAuth error codes - Unknown errors map to generic message - Encode all URL parameters Refs #337 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,66 @@ describe("CallbackPage", (): void => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should sanitize unknown error codes to prevent open redirect", async (): Promise<void> => {
|
||||
// Malicious error parameter that could be used for XSS or redirect attacks
|
||||
mockSearchParams.set("error", "<script>alert('xss')</script>");
|
||||
|
||||
render(<CallbackPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should replace unknown error with generic authentication_error
|
||||
expect(mockPush).toHaveBeenCalledWith("/login?error=authentication_error");
|
||||
});
|
||||
});
|
||||
|
||||
it("should sanitize URL-like error codes to prevent open redirect", async (): Promise<void> => {
|
||||
// Attacker tries to inject a URL-like value
|
||||
mockSearchParams.set("error", "https://evil.com/phishing");
|
||||
|
||||
render(<CallbackPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith("/login?error=authentication_error");
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow valid OAuth 2.0 error codes", async (): Promise<void> => {
|
||||
const validErrors = [
|
||||
"access_denied",
|
||||
"invalid_request",
|
||||
"unauthorized_client",
|
||||
"server_error",
|
||||
"login_required",
|
||||
"consent_required",
|
||||
];
|
||||
|
||||
for (const errorCode of validErrors) {
|
||||
mockPush.mockClear();
|
||||
mockSearchParams.clear();
|
||||
mockSearchParams.set("error", errorCode);
|
||||
|
||||
const { unmount } = render(<CallbackPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith(`/login?error=${errorCode}`);
|
||||
});
|
||||
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("should encode special characters in error parameter", async (): Promise<void> => {
|
||||
// Even valid errors should be encoded in the URL
|
||||
mockSearchParams.set("error", "session_failed");
|
||||
|
||||
render(<CallbackPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
// session_failed doesn't need encoding but the function should still call encodeURIComponent
|
||||
expect(mockPush).toHaveBeenCalledWith("/login?error=session_failed");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle refresh session errors gracefully", async (): Promise<void> => {
|
||||
const mockRefreshSession = vi.fn().mockRejectedValue(new Error("Session error"));
|
||||
vi.mocked(useAuth).mockReturnValue({
|
||||
|
||||
Reference in New Issue
Block a user