Skip to content

Commit c09a4cd

Browse files
msukkariclaude
andcommitted
fix(web): validate OAuth redirect URLs to prevent XSS
Add validateOAuthRedirectUrl() to block dangerous protocols (javascript:, data:, vbscript:) before assigning to window.location.href in the OAuth consent screen. Fixes CodeQL alert #33 (js/xss-through-exception) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bae8381 commit c09a4cd

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

packages/web/src/app/oauth/authorize/components/consentScreen.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { approveAuthorization, denyAuthorization } from '@/ee/features/oauth/actions';
44
import { LoadingButton } from '@/components/ui/loading-button';
5-
import { isServiceError } from '@/lib/utils';
5+
import { isServiceError, validateOAuthRedirectUrl } from '@/lib/utils';
66
import { ClientIcon } from './clientIcon';
77
import Image from 'next/image';
88
import logo from '@/public/logo_512.png';
@@ -44,16 +44,24 @@ export function ConsentScreen({
4444
setPending('approve');
4545
const result = await approveAuthorization({ clientId, redirectUri, codeChallenge, resource, state });
4646
if (!isServiceError(result)) {
47-
toast({
48-
description: `✅ Authorization approved successfully. Redirecting...`,
49-
});
50-
window.location.href = result;
47+
const validatedUrl = validateOAuthRedirectUrl(result);
48+
if (validatedUrl) {
49+
toast({
50+
description: `✅ Authorization approved successfully. Redirecting...`,
51+
});
52+
window.location.href = validatedUrl;
53+
} else {
54+
toast({
55+
description: '❌ Invalid redirect URL. Authorization could not be completed.',
56+
});
57+
setPending(null);
58+
}
5159
} else {
5260
toast({
5361
description: `❌ Failed to approve authorization. ${result.message}`,
5462
});
63+
setPending(null);
5564
}
56-
setPending(null);
5765
};
5866

5967
const onDeny = async () => {
@@ -64,7 +72,15 @@ export function ConsentScreen({
6472
setPending(null);
6573
return;
6674
}
67-
window.location.href = result;
75+
const validatedUrl = validateOAuthRedirectUrl(result);
76+
if (validatedUrl) {
77+
window.location.href = validatedUrl;
78+
} else {
79+
toast({
80+
description: '❌ Invalid redirect URL. Could not complete the request.',
81+
});
82+
setPending(null);
83+
}
6884
};
6985

7086
return (

packages/web/src/lib/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,4 +589,24 @@ export const isHttpError = (error: unknown, status: number): boolean => {
589589
&& typeof error === 'object'
590590
&& 'status' in error
591591
&& error.status === status;
592+
}
593+
594+
/**
595+
* Validates an OAuth redirect URL to prevent open redirect and javascript: URI attacks.
596+
* Returns the validated URL if safe, or null if the URL is potentially malicious.
597+
*/
598+
export const validateOAuthRedirectUrl = (url: string): string | null => {
599+
try {
600+
const parsed = new URL(url);
601+
const protocol = parsed.protocol.toLowerCase();
602+
603+
const dangerousProtocols = ['javascript:', 'data:', 'vbscript:'];
604+
if (dangerousProtocols.includes(protocol)) {
605+
return null;
606+
}
607+
608+
return parsed.toString();
609+
} catch {
610+
return null;
611+
}
592612
}

0 commit comments

Comments
 (0)