Skip to content

Validate the scheme before shell.openExternal - #72

Open
rafaelfiguereod-stack wants to merge 1 commit into
diffusionstudio:mainfrom
rafaelfiguereod-stack:fix/validate-open-external-scheme
Open

rafaelfiguereod-stack wants to merge 1 commit into
diffusionstudio:mainfrom
rafaelfiguereod-stack:fix/validate-open-external-scheme

Conversation

@rafaelfiguereod-stack

Copy link
Copy Markdown

Fixes the hardening report in #71.

The problem

APP_OPEN_EXTERNAL passed the renderer's URL straight to the shell:

mainBridge.handle(MAIN_CHANNELS.APP_OPEN_EXTERNAL, ({ url }) => shell.openExternal(url));

The chat's markdown renderer sends it anchor.href for any link the assistant writes. A relative link has no scheme for DOMPurify to reject, so it survives and resolves against the renderer's base. In a packaged build that base is file://, so [x](../../../Downloads/payload.exe) reaches this handler as an absolute path on disk, and shell.openExternal opens it with the OS handler.

Explicit file:///..., javascript: and custom schemes were already stripped by the sanitizer. Relative links were the way through.

The change

A small predicate in apps/desktop/src/external-url.ts, and the handler consults it. Only http:, https: and mailto: get through.

That covers every link the app opens today: the sign-in redirect in auth.tsx, checkout and the billing portal in checkout.ts, and ordinary links in chat. I included mailto: because DOMPurify already permits it and you have support addresses in the UI, so an assistant writing one should keep working. If you would rather be strict, deleting "mailto:" from the set is the whole change.

A refused link is logged by scheme alone, never the full URL, so a token in a rejected link stays out of the terminal.

I put the check in main rather than in the renderer's click handler on purpose. Main is the trust boundary, so it is the side that still holds if the renderer is ever wrong. A renderer side check would give a nicer no-op on click and could be added on top.

Verified

Windows 11, Node 24.1.0, Electron 43, on top of b317412.

I reproduced the original chain first, by running the repo's own render() and DOMPurify hook bundled against the repo's dompurify and marked inside a BrowserWindow loaded over file://, and reading back anchor.href. Then I fed those exact observed hrefs through the new predicate:

PASS  relative traversal   allowed=false expected=false
PASS  relative sibling     allowed=false expected=false
PASS  ordinary https       allowed=true  expected=true
PASS  oauth redirect       allowed=true  expected=true
PASS  support mailto       allowed=true  expected=true

Also:

  • npm run check --workspace=@diffusionstudio/desktop: clean
  • npm test --workspace=@diffusionstudio/desktop: 63 passed across 9 files, up from 59 across 8, the 4 new ones are external-url.test.ts
  • Rebuilt dist/main.js and launched the app, which comes up and answers dapi context and dapi screenshot as before

One note on the branch: this is cut from main, so npm run check across all workspaces still fails here for the unrelated missing type dependency in #70. The desktop workspace itself is clean.

🤖 Generated with Claude Code

The APP_OPEN_EXTERNAL handler passed the renderer's URL straight to the
shell. The chat's markdown renderer sends it anchor.href for any link the
assistant wrote, and a relative link has no scheme for DOMPurify to reject,
so it resolves against the renderer's base. In a packaged build that base is
file://, which makes [x](../../payload.exe) arrive here as a path on disk
the shell would open.

Only http, https and mailto get through now. Those cover every link the app
opens: the sign-in redirect, checkout, the billing portal, and ordinary
links in chat. A refused link is logged by scheme alone, so a token in a
rejected URL stays out of the log.

Explicit file://, javascript: and custom schemes were already stripped by
the sanitizer, so relative links were the way through.

Refs diffusionstudio#71

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
editor-web Skipped Skipped Sep 17, 2026 5:34pm UTC

This branch was previously deployed

1 inactive deployment
Preview 5f04e10f Deployed Sep 17, 2026 by vercel[bot]
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.

1 participant