Skip to content

Restrict markdown link navigation to safe URI schemes #508

Description

@quintessence-proof

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions