Skip to content

fix(electron): validate IPC sender frame for token-cache and OAuth handlers#9167

Open
dominic-clerk wants to merge 2 commits into
mainfrom
dc-validate-ipc-sender
Open

fix(electron): validate IPC sender frame for token-cache and OAuth handlers#9167
dominic-clerk wants to merge 2 commits into
mainfrom
dc-validate-ipc-sender

Conversation

@dominic-clerk

@dominic-clerk dominic-clerk commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

Token-cache and OAuth-transport IPC handlers accepted requests from any renderer frame. Untrusted content in a subframe or sharing the Clerk preload could read the persisted client JWT or drive the OAuth transport. Guard both against non-main-frame senders, mirroring the existing passkey handler check via a shared isMainFrameEvent helper.

Fixes SDK-142

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Security
    • Restricted token-cache, OAuth transport, and passkey IPC operations to requests originating from the app’s main window frame.
    • Blocked untrusted subframes and embedded webviews from accessing stored tokens or initiating OAuth/passkey flows.
    • Prevented redirect handling and external URL launching when triggered from non-main-frame requests.
  • Bug Fixes
    • Strengthened validation and expanded test coverage to ensure these main-frame protections are correctly enforced and cleaned up.

…ndlers

Token-cache and OAuth-transport IPC handlers accepted requests from any
renderer frame. Untrusted content in a subframe or <webview> sharing the
Clerk preload could read the persisted client JWT or drive the OAuth
transport. Guard both against non-main-frame senders, mirroring the
existing passkey handler check via a shared isMainFrameEvent helper.
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6610740

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

This PR includes changesets to release 1 package
Name Type
@clerk/electron 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

@vercel

vercel Bot commented Jul 15, 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 Jul 16, 2026 8:19am
swingset Ready Ready Preview, Comment Jul 16, 2026 8:19am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 15, 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 Plus

Run ID: fd633d94-93e5-469c-ac82-b069f35a92f1

📥 Commits

Reviewing files that changed from the base of the PR and between 5052474 and 6610740.

📒 Files selected for processing (5)
  • .changeset/fine-results-crash.md
  • packages/electron/src/main/__tests__/create-clerk-bridge.test.ts
  • packages/electron/src/main/__tests__/ipc-handlers.test.ts
  • packages/electron/src/main/__tests__/passkey-handlers.test.ts
  • packages/electron/src/main/validate-sender.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/fine-results-crash.md
  • packages/electron/src/main/validate-sender.ts
  • packages/electron/src/main/tests/create-clerk-bridge.test.ts

📝 Walkthrough

Walkthrough

Token-cache and OAuth IPC handlers now require requests to originate from a window’s main frame. Passkey validation reuses a shared helper, tests cover subframe and webview rejection, and a patch changeset documents the update.

Changes

IPC frame-origin security

Layer / File(s) Summary
Shared frame validation and privileged handlers
packages/electron/src/main/validate-sender.ts, packages/electron/src/main/ipc-handlers.ts, packages/electron/src/main/passkey-handlers.ts
Adds isMainFrameEvent and applies main-frame validation to token-cache and passkey IPC handlers.
OAuth transport entrypoint validation
packages/electron/src/main/oauth-transport.ts
Validates main-frame origin for redirect URL and OAuth open requests before continuing.
Security regression coverage and release metadata
packages/electron/src/main/__tests__/*, .changeset/fine-results-crash.md
Adds main-frame, subframe, and webview fixtures, rejection assertions, prevented external launches, and patch release metadata.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RendererFrame
  participant IPCHandler
  participant isMainFrameEvent
  participant TokenStorage
  RendererFrame->>IPCHandler: invoke token or OAuth IPC
  IPCHandler->>isMainFrameEvent: validate sender frame
  isMainFrameEvent-->>IPCHandler: validation result
  IPCHandler->>TokenStorage: perform token operation
Loading

Suggested reviewers: wobsoriano

Poem

I’m a rabbit guarding the frame,
No subframe may borrow the claim.
Tokens stay tucked,
OAuth paths are checked,
Safe main-frame hops win the game.

🚥 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 title accurately summarizes the main Electron IPC hardening for token-cache and OAuth handlers.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@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.

🧹 Nitpick comments (1)
packages/electron/src/main/ipc-handlers.ts (1)

8-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate "assert main frame or throw" logic across ipc-handlers.ts and oauth-transport.ts.

Both files re-implement the same check-and-throw wrapper around isMainFrameEvent instead of sharing one helper from validate-sender.ts. Consolidating avoids the two implementations drifting apart if the rejection behavior ever changes.

  • packages/electron/src/main/ipc-handlers.ts#L8-L12: move this assertMainFrameSender logic into validate-sender.ts (e.g. accepting a context string for the message) and import it here instead of defining it locally.
  • packages/electron/src/main/oauth-transport.ts#L95-L106: replace both inlined if (!isMainFrameEvent(event)) { throw ... } blocks with a call to the same shared assert helper.
♻️ Proposed shared helper
 // packages/electron/src/main/validate-sender.ts
 export function isMainFrameEvent(event: IpcMainInvokeEvent): boolean {
   return Boolean(event.senderFrame) && event.senderFrame === event.sender.mainFrame;
 }
+
+export function assertMainFrameSender(event: IpcMainInvokeEvent, context: string): void {
+  if (!isMainFrameEvent(event)) {
+    throw new Error(`Clerk: ${context} request did not originate from a window's main frame.`);
+  }
+}
 // packages/electron/src/main/ipc-handlers.ts
-function assertMainFrameSender(event: IpcMainInvokeEvent): void {
-  if (!isMainFrameEvent(event)) {
-    throw new Error("Clerk: token-cache request did not originate from a window's main frame.");
-  }
-}
+import { assertMainFrameSender } from './validate-sender';

(call sites become assertMainFrameSender(event, 'token-cache'))

 // packages/electron/src/main/oauth-transport.ts
   ipcMain.handle(OAUTH_TRANSPORT_CHANNELS.getRedirectUrl, event => {
-    if (!isMainFrameEvent(event)) {
-      throw new Error("Clerk: OAuth request did not originate from a window's main frame.");
-    }
+    assertMainFrameSender(event, 'OAuth');
     return redirectUrl;
   });

   ipcMain.handle(OAUTH_TRANSPORT_CHANNELS.open, async (event, url: string) => {
-    if (!isMainFrameEvent(event)) {
-      throw new Error("Clerk: OAuth request did not originate from a window's main frame.");
-    }
+    assertMainFrameSender(event, 'OAuth');
🤖 Prompt for 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.

In `@packages/electron/src/main/ipc-handlers.ts` around lines 8 - 12, Consolidate
the duplicated main-frame validation by moving assertMainFrameSender into
validate-sender.ts with a context parameter for the rejection message. In
packages/electron/src/main/ipc-handlers.ts lines 8-12, remove the local
implementation and import the shared helper; in
packages/electron/src/main/oauth-transport.ts lines 95-106, replace both inline
isMainFrameEvent checks with calls to that helper, preserving each request’s
context-specific message.
🤖 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.

Nitpick comments:
In `@packages/electron/src/main/ipc-handlers.ts`:
- Around line 8-12: Consolidate the duplicated main-frame validation by moving
assertMainFrameSender into validate-sender.ts with a context parameter for the
rejection message. In packages/electron/src/main/ipc-handlers.ts lines 8-12,
remove the local implementation and import the shared helper; in
packages/electron/src/main/oauth-transport.ts lines 95-106, replace both inline
isMainFrameEvent checks with calls to that helper, preserving each request’s
context-specific message.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: f4eb8873-fd8a-4f60-87ca-856e7f334f00

📥 Commits

Reviewing files that changed from the base of the PR and between 99ac79f and 5052474.

📒 Files selected for processing (7)
  • .changeset/fine-results-crash.md
  • packages/electron/src/main/__tests__/create-clerk-bridge.test.ts
  • packages/electron/src/main/__tests__/ipc-handlers.test.ts
  • packages/electron/src/main/ipc-handlers.ts
  • packages/electron/src/main/oauth-transport.ts
  • packages/electron/src/main/passkey-handlers.ts
  • packages/electron/src/main/validate-sender.ts

@pkg-pr-new

pkg-pr-new Bot commented Jul 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/electron

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

@clerk/electron-passkeys

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

@clerk/eslint-plugin

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

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: 6610740

Comment thread packages/electron/src/main/validate-sender.ts Outdated
@dominic-clerk dominic-clerk removed the request for review from jeremy-clerk July 15, 2026 12:28
@wobsoriano wobsoriano self-requested a review July 15, 2026 18:16
isMainFrameEvent only proved the sender was the top document of its own
WebContents, so a <webview> (a separate WebContents) sharing the Clerk
preload satisfied senderFrame === mainFrame and passed the guard. Require
getType() === 'window' to tie privileged IPC to a top-level BrowserWindow,
rejecting <webview> guests and BrowserViews.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremy-clerk

Copy link
Copy Markdown
Contributor

Good call. Looks good from reading the code. Will run validation e2e to ensure no breakage. Thank you!

@jeremy-clerk jeremy-clerk 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.

👍 want to get @wobsoriano's input as well though

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants