diff --git a/packages/web/src/app/oauth/authorize/components/consentScreen.tsx b/packages/web/src/app/oauth/authorize/components/consentScreen.tsx index 8d22d126c..8cf46ad64 100644 --- a/packages/web/src/app/oauth/authorize/components/consentScreen.tsx +++ b/packages/web/src/app/oauth/authorize/components/consentScreen.tsx @@ -2,7 +2,7 @@ import { approveAuthorization, denyAuthorization } from '@/ee/features/oauth/actions'; import { LoadingButton } from '@/components/ui/loading-button'; -import { isServiceError } from '@/lib/utils'; +import { isServiceError, validateOAuthRedirectUrl } from '@/lib/utils'; import { ClientIcon } from './clientIcon'; import Image from 'next/image'; import logo from '@/public/logo_512.png'; @@ -44,16 +44,24 @@ export function ConsentScreen({ setPending('approve'); const result = await approveAuthorization({ clientId, redirectUri, codeChallenge, resource, state }); if (!isServiceError(result)) { - toast({ - description: `✅ Authorization approved successfully. Redirecting...`, - }); - window.location.href = result; + const validatedUrl = validateOAuthRedirectUrl(result); + if (validatedUrl) { + toast({ + description: `✅ Authorization approved successfully. Redirecting...`, + }); + window.location.href = validatedUrl; + } else { + toast({ + description: '❌ Invalid redirect URL. Authorization could not be completed.', + }); + setPending(null); + } } else { toast({ description: `❌ Failed to approve authorization. ${result.message}`, }); + setPending(null); } - setPending(null); }; const onDeny = async () => { @@ -64,7 +72,15 @@ export function ConsentScreen({ setPending(null); return; } - window.location.href = result; + const validatedUrl = validateOAuthRedirectUrl(result); + if (validatedUrl) { + window.location.href = validatedUrl; + } else { + toast({ + description: '❌ Invalid redirect URL. Could not complete the request.', + }); + setPending(null); + } }; return ( diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index 6a13ca9e7..a64eb4b13 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -589,4 +589,24 @@ export const isHttpError = (error: unknown, status: number): boolean => { && typeof error === 'object' && 'status' in error && error.status === status; +} + +/** + * Validates an OAuth redirect URL to prevent open redirect and javascript: URI attacks. + * Returns the validated URL if safe, or null if the URL is potentially malicious. + */ +export const validateOAuthRedirectUrl = (url: string): string | null => { + try { + const parsed = new URL(url); + const protocol = parsed.protocol.toLowerCase(); + + const dangerousProtocols = ['javascript:', 'data:', 'vbscript:']; + if (dangerousProtocols.includes(protocol)) { + return null; + } + + return parsed.toString(); + } catch { + return null; + } } \ No newline at end of file