Skip to content

fix(clerk-js): validate window navigation protocols and honor allowedRedirectProtocols#8961

Open
jacekradko wants to merge 1 commit into
mainfrom
jacek/validate-window-navigation-protocols
Open

fix(clerk-js): validate window navigation protocols and honor allowedRedirectProtocols#8961
jacekradko wants to merge 1 commit into
mainfrom
jacek/validate-window-navigation-protocols

Conversation

@jacekradko

@jacekradko jacekradko commented Jun 23, 2026

Copy link
Copy Markdown
Member

windowNavigate in @clerk/shared assigned its argument straight to window.location.href on the assumption that callers had already validated it. This change routes every internal browser navigation in clerk-js and @clerk/ui through one chokepoint, clerk.__internal_windowNavigate, which checks the resolved URL against the configured allowedRedirectProtocols (the http/https/wails/chrome-extension defaults plus any customer additions) and rejects scheme-relative inputs like //host before parsing.

The part worth a close look is the version-skew path: @clerk/ui can run against an older clerk-js that does not expose __internal_windowNavigate, so clerkWindowNavigate falls back to the bare @clerk/shared helper and the static allowlist, still rejecting the bad cases. The bare windowNavigate export stays for that reason and is now @deprecated in favor of the wrapper.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed missing redirect URL protocol validation for browser navigations, including multi-session add-account, so redirects only use permitted protocols (“fail closed” for unsafe schemes).
    • Improved consistency of full-page navigation during sign-in/sign-up and external verification flows, including when mixed component versions are in use.
    • Updated UI navigation to prefer the Clerk instance–aware navigation path when available.
  • Tests

    • Added/expanded unit tests covering allowed/blocked protocols, scheme-relative inputs, and fallback behavior when the internal navigation hook is unavailable.

@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 23, 2026 3:22am
swingset Ready Ready Preview, Comment Jun 23, 2026 3:22am

Request Review

@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 16b7d03

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@clerk/clerk-js Patch
@clerk/react Patch
@clerk/shared Patch
@clerk/ui Patch
@clerk/chrome-extension Patch
@clerk/electron Patch
@clerk/expo Patch
@clerk/nextjs Patch
@clerk/react-router Patch
@clerk/tanstack-react-start Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/headless Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nuxt Patch
@clerk/testing Patch
@clerk/vue Patch
@clerk/swingset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: a47c1d30-ef05-46e4-b336-95ead31ac503

📥 Commits

Reviewing files that changed from the base of the PR and between 805453a and 16b7d03.

📒 Files selected for processing (16)
  • .changeset/windownavigate-protocol-allowlist.md
  • packages/clerk-js/bundlewatch.config.json
  • packages/clerk-js/src/core/clerk.ts
  • packages/clerk-js/src/core/resources/SignIn.ts
  • packages/clerk-js/src/core/resources/SignUp.ts
  • packages/react/src/isomorphicClerk.ts
  • packages/shared/src/internal/clerk-js/__tests__/windowNavigate.test.ts
  • packages/shared/src/internal/clerk-js/windowNavigate.ts
  • packages/shared/src/types/clerk.ts
  • packages/ui/src/components/UserButton/useMultisessionActions.tsx
  • packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx
  • packages/ui/src/contexts/components/SessionTasks.ts
  • packages/ui/src/contexts/components/SignIn.ts
  • packages/ui/src/contexts/components/SignUp.ts
  • packages/ui/src/utils/__tests__/windowNavigate.test.ts
  • packages/ui/src/utils/windowNavigate.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/clerk-js/bundlewatch.config.json
  • .changeset/windownavigate-protocol-allowlist.md
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/shared/src/types/clerk.ts
  • packages/ui/src/components/UserButton/useMultisessionActions.tsx
  • packages/ui/src/utils/windowNavigate.ts
  • packages/ui/src/contexts/components/SignIn.ts
  • packages/react/src/isomorphicClerk.ts
  • packages/ui/src/contexts/components/SessionTasks.ts
  • packages/ui/src/utils/tests/windowNavigate.test.ts
  • packages/clerk-js/src/core/clerk.ts
  • packages/clerk-js/src/core/resources/SignIn.ts
  • packages/shared/src/internal/clerk-js/windowNavigate.ts
  • packages/clerk-js/src/core/resources/SignUp.ts
  • packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx
  • packages/shared/src/internal/clerk-js/tests/windowNavigate.test.ts
  • packages/ui/src/contexts/components/SignUp.ts

📝 Walkthrough

Walkthrough

A new __internal_windowNavigate chokepoint is added to Clerk and the shared Clerk interface. The windowNavigate utility gains scheme-relative rejection and protocol allowlist validation. All ClerkJS resource and UI component call sites are migrated from direct windowNavigate calls to the Clerk-instance-aware clerkWindowNavigate bridge, which falls back gracefully for older ClerkJS bundles.

Changes

Window navigation protocol allowlist enforcement

Layer / File(s) Summary
Shared types and Clerk interface contract
packages/shared/src/internal/clerk-js/windowNavigate.ts, packages/shared/src/types/clerk.ts, .changeset/windownavigate-protocol-allowlist.md
WindowNavigateOptions type with optional allowedProtocols is added. Clerk interface gains __internal_windowNavigate(to, opts?) method signature with useStaticAllowlistOnly flag. Changeset records patch releases.
Hardened windowNavigate implementation and tests
packages/shared/src/internal/clerk-js/windowNavigate.ts, packages/shared/src/internal/clerk-js/__tests__/windowNavigate.test.ts
windowNavigate now rejects scheme-relative inputs, resolves the URL, selects an effective allowlist, and blocks navigation with a warning for disallowed protocols. Tests cover allowed schemes, blocked schemes, scheme-relative forms, extended allowlists, and fail-closed behavior.
Clerk.__internal_windowNavigate method and navigate() wiring
packages/clerk-js/src/core/clerk.ts, packages/clerk-js/src/core/resources/SignIn.ts, packages/clerk-js/src/core/resources/SignUp.ts
Clerk gains public __internal_windowNavigate that computes the protocol allowlist and delegates to windowNavigate. navigate() routes custom-protocol URLs and customNavigate metadata through this method. SignIn and SignUp resources remove direct windowNavigate imports and call clerk.__internal_windowNavigate instead.
clerkWindowNavigate UI bridge, IsomorphicClerk forwarding, and tests
packages/ui/src/utils/windowNavigate.ts, packages/react/src/isomorphicClerk.ts, packages/ui/src/utils/__tests__/windowNavigate.test.ts
clerkWindowNavigate(clerk, to, opts) delegates to clerk.__internal_windowNavigate when present or falls back to the static-allowlist windowNavigate for older ClerkJS bundles. IsomorphicClerk forwards __internal_windowNavigate via optional chaining. Tests verify delegation, fallback, and fail-closed behavior.
UI component call sites migrated to clerkWindowNavigate
packages/ui/src/components/UserButton/useMultisessionActions.tsx, packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx, packages/ui/src/contexts/components/SessionTasks.ts, packages/ui/src/contexts/components/SignIn.ts, packages/ui/src/contexts/components/SignUp.ts
All affected UI components and contexts replace direct windowNavigate calls with clerkWindowNavigate(clerk, ...), obtaining the clerk instance from useClerk() where needed.
Bundle size configuration updates
packages/clerk-js/bundlewatch.config.json
Bundle size limits for clerk.legacy.browser.js and clerk.native.js are increased to accommodate the new __internal_windowNavigate method.

Sequence Diagram(s)

sequenceDiagram
  participant UIComponent as UI Component
  participant bridge as clerkWindowNavigate()
  participant chokepoint as Clerk.__internal_windowNavigate()
  participant core as windowNavigate()
  
  rect rgba(70, 130, 180, 0.5)
    Note over UIComponent,chokepoint: New ClerkJS bundle path
    UIComponent->>bridge: clerkWindowNavigate(clerk, url)
    bridge->>chokepoint: call(clerk, url, opts)
    chokepoint->>chokepoint: compute allowedProtocols<br/>(static ± customer extension)
    chokepoint->>core: windowNavigate(url, {allowedProtocols})
    core-->>UIComponent: navigated or blocked+warned
  end

  rect rgba(180, 100, 70, 0.5)
    Note over UIComponent,core: Older ClerkJS fallback path
    UIComponent->>bridge: clerkWindowNavigate(clerk, url)
    bridge->>core: windowNavigate(url) [static allowlist only]
    core-->>UIComponent: navigated or blocked+warned (fail closed)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • tmilewski

Poem

🐇 A rabbit hops through URLs with care,
Checking each protocol—"Is javascript: there? Beware!"
__internal_windowNavigate guards the gate,
Scheme-relative tricks meet a strict, closed fate.
No sneaky \\host or data: shall pass,
Only safe schemes navigate at last! 🔒

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: implementing protocol validation for window navigation and honoring allowedRedirectProtocols settings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8961

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8961

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8961

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8961

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@8961

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@8961

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8961

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8961

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8961

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8961

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8961

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8961

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8961

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8961

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8961

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8961

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8961

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8961

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8961

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8961

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8961

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8961

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8961

commit: 16b7d03

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-23T03:23:46.020Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 2
🔴 Breaking changes 0
🟡 Non-breaking changes 1
🟢 Additions 3

🤖 This report was reviewed by claude-sonnet-4-6.


@clerk/clerk-js

Current version: 6.20.0
Recommended bump: MINOR → 6.21.0

Subpath .

🟢 Additions (1)

Added: Clerk.__internal_windowNavigate
+ __internal_windowNavigate: (to: URL | string, opts?: {
+         useStaticAllowlistOnly?: boolean;
+     }) => void;

Added property Clerk.__internal_windowNavigate

Subpath ./no-rhc

🟢 Additions (1)

Added: Clerk.__internal_windowNavigate
+ __internal_windowNavigate: (to: URL | string, opts?: {
+         useStaticAllowlistOnly?: boolean;
+     }) => void;

Added property Clerk.__internal_windowNavigate


@clerk/shared

Current version: 4.20.0
Recommended bump: MINOR → 4.21.0

Subpath ./internal/clerk-js/windowNavigate

🟡 Non-breaking Changes (1)

Modified: windowNavigate
- declare function windowNavigate(to: URL | string): void;
+ declare function windowNavigate(to: URL | string, options?: WindowNavigateOptions): void;

Static analyzer: Modified function windowNavigate: Optional parameter options was added

🤖 AI review (confirmed) (100%): Adding an optional parameter options to windowNavigate does not affect existing call sites, which pass no second argument and remain valid.

🟢 Additions (1)

Added: WindowNavigateOptions
+ type WindowNavigateOptions = {
+   allowedProtocols?: ReadonlyArray<string>;
+ };

Added type alias WindowNavigateOptions


Report generated by Break Check

Last ran on 16b7d03.

@coderabbitai coderabbitai Bot 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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react/src/isomorphicClerk.ts`:
- Around line 268-270: The `__internal_windowNavigate` method in IsomorphicClerk
always exists as a forwarder, but when the clerkjs version lacks this method,
the optional chain silently fails and navigation is dropped without fallback.
Modify the `__internal_windowNavigate` implementation to check if clerkjs
actually supports this method before delegating to it, and provide an
appropriate fallback behavior (such as returning false or implementing
alternative navigation logic) when the method is not available on the underlying
clerkjs instance, ensuring that older clerkjs versions can still handle
navigation through their default mechanisms.
🪄 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: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: e7aa3b33-c676-4786-b718-c746653758d4

📥 Commits

Reviewing files that changed from the base of the PR and between 52d310c and 0dbc430.

📒 Files selected for processing (15)
  • .changeset/windownavigate-protocol-allowlist.md
  • packages/clerk-js/src/core/clerk.ts
  • packages/clerk-js/src/core/resources/SignIn.ts
  • packages/clerk-js/src/core/resources/SignUp.ts
  • packages/react/src/isomorphicClerk.ts
  • packages/shared/src/internal/clerk-js/__tests__/windowNavigate.test.ts
  • packages/shared/src/internal/clerk-js/windowNavigate.ts
  • packages/shared/src/types/clerk.ts
  • packages/ui/src/components/UserButton/useMultisessionActions.tsx
  • packages/ui/src/components/UserProfile/EnterpriseAccountsSection.tsx
  • packages/ui/src/contexts/components/SessionTasks.ts
  • packages/ui/src/contexts/components/SignIn.ts
  • packages/ui/src/contexts/components/SignUp.ts
  • packages/ui/src/utils/__tests__/windowNavigate.test.ts
  • packages/ui/src/utils/windowNavigate.ts

Comment thread packages/react/src/isomorphicClerk.ts
*/
export function windowNavigate(to: URL | string): void {
export function windowNavigate(to: URL | string, options?: WindowNavigateOptions): void {
if (typeof to === 'string' && SCHEME_RELATIVE_PREFIX.test(to.trim())) {

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.

Scheme-relative open-redirect bypass via control characters

trim() only strips leading/trailing whitespace, but new URL() (next line) strips tab/LF/CR from anywhere and leading C0 controls — so an interior or leading control char defeats this regex while still resolving to a scheme-relative URL. It then inherits the base https: scheme (allowlisted), so navigation proceeds cross-origin.

Bypasses against base https://victim-app.com/:

"/\t/evil.attacker.com"   // navigates cross-origin
"\x00//evil.attacker.com" // navigates cross-origin
"//evil.attacker.com"     // correctly blocked (only case covered)

The added tests only cover leading/trailing whitespace, which trim() handles, so this slips through.

Fix — strip parser-ignored chars before the test:

if (typeof to === 'string' && SCHEME_RELATIVE_PREFIX.test(to.replace(/[\u0000-\u001F]/g, ''))) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants