File: src/ui/file-preview/src/markdown/linking.ts
Problem
isExternalHref() currently classifies anything with a scheme-like prefix as external:
// src/ui/file-preview/src/markdown/linking.ts (current)
function isExternalHref(rawHref: string): boolean {
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(rawHref) && !isWindowsAbsolutePath(rawHref);
}
This regex matches javascript:, data:, vbscript:, and file: as readily as
https:. A matched link is classified kind: 'external' and passed directly to
openExternalLink() or window.open() in controller.ts.
Browsers block javascript: in window.open() for now, but that is a downstream
assumption, not an explicit guarantee in this codebase. A small allowlist removes the
dependency on that external behaviour.
Patch
Replace the existing isExternalHref with the version below, or add the scheme check
as a guard directly in resolveMarkdownLink before the kind: 'external' return.
// ── Safe-scheme allowlist ─────────────────────────────────────────────────────
/** Schemes that are permitted to open as external links. */
const ALLOWED_EXTERNAL_SCHEMES = new Set([
'http',
'https',
'mailto',
'ftp', // keep for completeness; rare in practice
]);
/**
* Returns true only for URLs with a scheme we explicitly permit.
* Prevents javascript:, data:, vbscript:, file:, blob: etc. from being
* treated as navigable external links.
*/
function isExternalHref(rawHref: string): boolean {
if (isWindowsAbsolutePath(rawHref)) return false;
const match = rawHref.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
if (!match) return false;
return ALLOWED_EXTERNAL_SCHEMES.has(match[1].toLowerCase());
}
No other changes are needed — resolveMarkdownLink already uses isExternalHref as
its gate, and anything that doesn't match external, anchor, or a Windows absolute
path falls through to the file classification (which is safe — it opens a local file
via the host's file-read tools).
What changes for users
Links using javascript:, data:, file:, blob:, or any other non-listed scheme
will no longer trigger navigation. They silently become no-ops on click.
http:, https:, and mailto: links are unaffected.
Add to ALLOWED_EXTERNAL_SCHEMES if additional schemes need to be navigable.
Test cases
// Should classify as external (allowed):
isExternalHref('https://example.com') // true
isExternalHref('http://example.com') // true
isExternalHref('mailto:a@b.com') // true
// Should NOT classify as external (blocked):
isExternalHref('javascript:alert(1)') // false
isExternalHref('data:text/html,<h1>x</h1>') // false
isExternalHref('vbscript:msgbox(1)') // false
isExternalHref('file:///etc/passwd') // false
isExternalHref('blob:https://…') // false
isExternalHref('/relative/path') // false (no scheme)
isExternalHref('#anchor') // false (no scheme)
File:
src/ui/file-preview/src/markdown/linking.tsProblem
isExternalHref()currently classifies anything with a scheme-like prefix as external:This regex matches
javascript:,data:,vbscript:, andfile:as readily ashttps:. A matched link is classifiedkind: 'external'and passed directly toopenExternalLink()orwindow.open()incontroller.ts.Browsers block
javascript:inwindow.open()for now, but that is a downstreamassumption, not an explicit guarantee in this codebase. A small allowlist removes the
dependency on that external behaviour.
Patch
Replace the existing
isExternalHrefwith the version below, or add the scheme checkas a guard directly in
resolveMarkdownLinkbefore thekind: 'external'return.No other changes are needed —
resolveMarkdownLinkalready usesisExternalHrefasits gate, and anything that doesn't match
external,anchor, or a Windows absolutepath falls through to the
fileclassification (which is safe — it opens a local filevia the host's file-read tools).
What changes for users
Links using
javascript:,data:,file:,blob:, or any other non-listed schemewill no longer trigger navigation. They silently become no-ops on click.
http:,https:, andmailto:links are unaffected.Add to
ALLOWED_EXTERNAL_SCHEMESif additional schemes need to be navigable.Test cases