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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
msukkari merged 2 commits into
mainfrom
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 into
mainfrom
michael/fix-oauth-xss-SOU-928

Conversation

@msukkari

@msukkarimsukkari commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitaiBot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe 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✅ PassedCheck 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 threadpackages/web/src/app/oauth/complete/page.tsx Dismissed
Comment threadpackages/web/src/app/oauth/complete/page.tsx Dismissed

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadpackages/web/src/ee/features/oauth/constants.ts
@msukkari
msukkari merged commit b2941c4 into mainApr 19, 2026
13 checks passed
@msukkari
msukkari deleted the michael/fix-oauth-xss-SOU-928 branch April 19, 2026 00:44
@github-actionsgithub-actionsBot mentioned this pull request Apr 19, 2026
Sign up for freeto 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

@msukkari@github-advanced-security