Skip to content

fix(web): prevent XSS via OAuth redirect URI scheme injection#1136

Merged
msukkari merged 2 commits intomainfrom
michael/fix-oauth-xss-SOU-928
Apr 19, 2026
Merged

fix(web): prevent XSS via OAuth redirect URI scheme injection#1136
msukkari merged 2 commits intomainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari
Copy link
Copy Markdown
Contributor

@msukkari msukkari commented Apr 19, 2026

Fixes SOU-928

Summary

  • Block javascript:, data:, and vbscript: URI schemes across the full OAuth redirect flow to resolve CodeQL js/xss-through-exception alert
  • Add defense-in-depth validation at four layers: client registration, server-side callback resolution, client-side consent screen, and the /oauth/complete handoff page
  • Add shared UNPERMITTED_SCHEMES constant and isPermittedRedirectUrl helper in constants.ts for consistent validation across server and client
  • Add tests for UNPERMITTED_SCHEMES and isPermittedRedirectUrl covering all supported MCP OAuth flows (http, https, vscode://, cursor://, claude://)

Test plan

  • Verify OAuth approve flow works with http/https redirect URIs
  • Verify OAuth approve flow works with custom scheme redirect URIs (vscode://, cursor://)
  • Verify OAuth deny flow works with http/https redirect URIs
  • Verify OAuth deny flow works with custom scheme redirect URIs
  • Verify javascript: redirect URIs are rejected at client registration
  • Verify /oauth/complete page shows error for blocked schemes
  • Run yarn workspace @sourcebot/web test — all 295 tests pass
  • Run yarn workspace @sourcebot/web build — production build succeeds

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed XSS vulnerability in OAuth redirect flow by blocking dangerous URI schemes (javascript:, data:, vbscript:) across registration, authorization, and redirect stages.
  • Tests

    • Added comprehensive test suite for redirect URL validation security logic.

Block javascript:, data:, and vbscript: URI schemes across the OAuth
redirect flow to resolve CodeQL js/xss-through-exception alert. Adds
defense-in-depth validation at four layers: client registration,
server-side callback resolution, client-side consent screen, and the
/oauth/complete handoff page.

Fixes SOU-928

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

This comment has been minimized.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 19, 2026

Walkthrough

A security remediation that blocks dangerous URI schemes (javascript:, data:, vbscript:) across the OAuth lifecycle. Validation is added at the registration, authorization, and completion stages to prevent XSS attacks through OAuth redirect URIs.

Changes

Cohort / File(s) Summary
Redirect Validation Constants & Tests
packages/web/src/ee/features/oauth/constants.ts, packages/web/src/ee/features/oauth/constants.test.ts
Introduced UNPERMITTED_SCHEMES regex and isPermittedRedirectUrl() function to validate redirect URIs. Added comprehensive test coverage validating rejection of dangerous schemes and acceptance of safe URLs.
OAuth Registration Handler
packages/web/src/app/api/(server)/ee/oauth/register/route.ts
Added runtime validation in POST handler to reject redirect_uris matching unpermitted schemes, returning a 400 error with invalid_redirect_uri code before database operations.
Authorization & Completion Validation
packages/web/src/app/oauth/authorize/components/consentScreen.tsx, packages/web/src/app/oauth/complete/page.tsx, packages/web/src/ee/features/oauth/actions.ts
Added client-side and server-side redirect URL validation in authorization approval/denial flows and completion page. Validation occurs before window.location.href redirect and includes error handling with user feedback.
Documentation
CHANGELOG.md
Added unreleased changelog entry documenting the XSS vulnerability fix for OAuth redirect URI scheme blocking.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(web): prevent XSS via OAuth redirect URI scheme injection' directly and clearly describes the main security fix—blocking dangerous URI schemes in OAuth redirects to prevent XSS vulnerabilities. It is specific, concise, and accurately summarizes the primary change across all modified files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch michael/fix-oauth-xss-SOU-928

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread packages/web/src/app/oauth/complete/page.tsx Dismissed
Comment thread packages/web/src/app/oauth/complete/page.tsx Dismissed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/app/oauth/authorize/components/consentScreen.tsx (1)

69-72: ⚠️ Potential issue | 🟡 Minor

Show an error when deny fails.

Line 70 now can receive a ServiceError from the new server-side scheme guard, but the deny path silently returns. Mirror the approve path so users are not left with no feedback.

💬 Proposed fix
         const result = await denyAuthorization({ redirectUri, state });
         if (isServiceError(result)) {
+            toast({
+                description: `❌ Failed to deny authorization. ${result.message}`,
+            });
             setPending(null);
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/authorize/components/consentScreen.tsx` around
lines 69 - 72, The denyAuthorization call can return a ServiceError but
currently the code just clears pending and returns silently; update the deny
path (where denyAuthorization, isServiceError, and setPending are used) to
mirror the approve flow by setting appropriate UI feedback when a ServiceError
occurs (e.g., call the same error handler or setError/toast used in the approve
branch) and then clear pending; ensure the user sees the error message instead
of a silent return.
♻️ Duplicate comments (1)
packages/web/src/app/oauth/complete/page.tsx (1)

13-23: ⚠️ Potential issue | 🔴 Critical

Parse and validate the target before assigning location.href.

Line 18 double-decodes because URLSearchParams.get() already returns a decoded value, and Line 19 checks raw text rather than the parsed protocol. A value such as %0Ajavascript:alert(1) can miss the regex but still parse as javascript: when navigated.

🛡️ Proposed fix
         const raw = new URLSearchParams(window.location.search).get('url');
         if (!raw) {
             setError('Missing redirect URL. You may close this window.');
             return;
         }
-        const decoded = decodeURIComponent(raw);
-        if (UNPERMITTED_SCHEMES.test(decoded)) {
+
+        let target: URL;
+        try {
+            target = new URL(raw);
+        } catch {
+            setError('Invalid redirect URL. You may close this window.');
+            return;
+        }
+
+        if (UNPERMITTED_SCHEMES.test(target.protocol)) {
             setError('Redirect URL is not permitted. You may close this window.');
             return;
         }
-        window.location.href = decoded;
+
+        window.location.href = target.toString();

Verification:

#!/bin/bash
# Description: Demonstrate that URLSearchParams already decodes and URL parsing normalizes a control-prefixed scheme.
node <<'NODE'
const raw = new URLSearchParams('url=%0Ajavascript%3Aalert(1)').get('url');
console.log({
  raw: JSON.stringify(raw),
  currentRegexMatches: /^(javascript|data|vbscript):/i.test(raw),
  parsedProtocol: new URL(raw).protocol,
});
NODE
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/complete/page.tsx` around lines 13 - 23, The code
double-decodes and validates the raw string instead of the parsed URL; replace
the decode-and-regex check with proper parsing and validation: retrieve the
value from URLSearchParams (do not call decodeURIComponent), attempt to
construct a new URL(target, window.location.href) inside a try/catch, extract
and test the URL.protocol against UNPERMITTED_SCHEMES (or invert the regex
check), call setError on parse failures or disallowed protocols, and only then
assign window.location.href = parsed.href; reference the existing
URLSearchParams.get('url'), decodeURIComponent usage, UNPERMITTED_SCHEMES, and
window.location.href locations when making the changes.
🧹 Nitpick comments (1)
packages/web/src/ee/features/oauth/constants.test.ts (1)

20-27: Add regression coverage for browser-trimmed dangerous schemes.

The current blocked-scheme tests only cover schemes at position 0. Please add cases with leading spaces/control characters so the OAuth XSS bypass cannot regress.

🧪 Proposed test additions
     test.each([
         'javascript:alert(1)',
         'data:text/html,<script>alert(1)</script>',
         'vbscript:MsgBox("xss")',
+        ' javascript:alert(1)',
+        '\njavascript:alert(1)',
+        '\tdata:text/html,<script>alert(1)</script>',
     ])('blocks full URL string: %s', (url) => {
         expect(UNPERMITTED_SCHEMES.test(url)).toBe(true);
     });
     test('blocks javascript: with mixed case', () => {
         expect(isPermittedRedirectUrl('JavaScript:alert(1)')).toBe(false);
     });
+
+    test('blocks dangerous schemes with leading whitespace/control characters', () => {
+        expect(isPermittedRedirectUrl('\njavascript:alert(1)')).toBe(false);
+        expect(isPermittedRedirectUrl(' data:text/html,<script>alert(1)</script>')).toBe(false);
+    });

Also applies to: 70-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/ee/features/oauth/constants.test.ts` around lines 20 - 27,
Add regression tests for browser-trimmed dangerous schemes by extending the
existing test.each that asserts UNPERMITTED_SCHEMES.test(url) to include
variants of the same dangerous schemes prefixed with leading whitespace and
control characters (e.g., spaces, tabs, CR/LF, %0A/%0D) so schemes like
'javascript:alert(1)' and 'data:...' still match after trimming; update both the
full-URL tests around UNPERMITTED_SCHEMES and the similar cases in the other
block that covers positions 70-84 to include these leading-character variants to
prevent an OAuth XSS bypass regression.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/ee/features/oauth/constants.ts`:
- Line 4: UNPERMITTED_SCHEMES currently only matches dangerous schemes at the
string start, so inputs with leading C0 controls or whitespace (e.g.
"%0ajavascript:") bypass it; update the UNPERMITTED_SCHEMES regex to allow and
skip any leading C0 control characters and whitespace before matching the scheme
(i.e. permit a leading character class for U+0000–U+001F and whitespace before
the (javascript|data|vbscript): group) so checks in /oauth/complete/page.tsx
catch normalized inputs.

---

Outside diff comments:
In `@packages/web/src/app/oauth/authorize/components/consentScreen.tsx`:
- Around line 69-72: The denyAuthorization call can return a ServiceError but
currently the code just clears pending and returns silently; update the deny
path (where denyAuthorization, isServiceError, and setPending are used) to
mirror the approve flow by setting appropriate UI feedback when a ServiceError
occurs (e.g., call the same error handler or setError/toast used in the approve
branch) and then clear pending; ensure the user sees the error message instead
of a silent return.

---

Duplicate comments:
In `@packages/web/src/app/oauth/complete/page.tsx`:
- Around line 13-23: The code double-decodes and validates the raw string
instead of the parsed URL; replace the decode-and-regex check with proper
parsing and validation: retrieve the value from URLSearchParams (do not call
decodeURIComponent), attempt to construct a new URL(target,
window.location.href) inside a try/catch, extract and test the URL.protocol
against UNPERMITTED_SCHEMES (or invert the regex check), call setError on parse
failures or disallowed protocols, and only then assign window.location.href =
parsed.href; reference the existing URLSearchParams.get('url'),
decodeURIComponent usage, UNPERMITTED_SCHEMES, and window.location.href
locations when making the changes.

---

Nitpick comments:
In `@packages/web/src/ee/features/oauth/constants.test.ts`:
- Around line 20-27: Add regression tests for browser-trimmed dangerous schemes
by extending the existing test.each that asserts UNPERMITTED_SCHEMES.test(url)
to include variants of the same dangerous schemes prefixed with leading
whitespace and control characters (e.g., spaces, tabs, CR/LF, %0A/%0D) so
schemes like 'javascript:alert(1)' and 'data:...' still match after trimming;
update both the full-URL tests around UNPERMITTED_SCHEMES and the similar cases
in the other block that covers positions 70-84 to include these
leading-character variants to prevent an OAuth XSS bypass regression.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d06977f7-b637-490f-93b5-71576f55e444

📥 Commits

Reviewing files that changed from the base of the PR and between beff3b7 and cb00385.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • packages/web/src/app/api/(server)/ee/oauth/register/route.ts
  • packages/web/src/app/oauth/authorize/components/consentScreen.tsx
  • packages/web/src/app/oauth/complete/page.tsx
  • packages/web/src/ee/features/oauth/actions.ts
  • packages/web/src/ee/features/oauth/constants.test.ts
  • packages/web/src/ee/features/oauth/constants.ts

Comment thread packages/web/src/ee/features/oauth/constants.ts
@msukkari msukkari merged commit b2941c4 into main Apr 19, 2026
13 checks passed
@msukkari msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants