Skip to content

fix(links): stop a document link from opening a local file through an asset URL - #415

Merged
PathGao merged 1 commit into
masterfrom
fix/asset-url-link-deception
Aug 3, 2026
Merged

fix(links): stop a document link from opening a local file through an asset URL#415
PathGao merged 1 commit into
masterfrom
fix/asset-url-link-deception

Conversation

@PathGao

@PathGao PathGao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

A link in a document can read as a remote address everywhere the user can see it, and open a local file.

[report](http://asset.localhost/Users/me/.ssh/id_rsa)

Measured against the real resolver on master:

"http://asset.localhost/etc/passwd"            -> "/etc/passwd"
"https://asset.localhost/Users/me/.ssh/id_rsa" -> "/Users/me/.ssh/id_rsa"
"asset://localhost/etc/hosts"                  -> "/etc/hosts"
"https://example.com/page"                     -> null        (unaffected)
"./data.csv"                                   -> "/docs/data.csv"  (unaffected)

That path then goes to openPath → the OS default handler. It became reachable in #403, which opened the opener path scope so open_path could run at all.

How it got there

resolveLocalFileLinkPath (#409) delegates to resolveExportImagePath (#363), and that was the right call — the scheme / drive-letter / UNC / query-suffix decision table is genuinely shared, and hand-rolling a second one is how the startsWith('http://asset.localhost') prefix-spoofing hole appeared in the first place.

But one clause of it does not transfer. normalizeAssetPath accepts asset URLs because that is the shape a local image's src takes inside the webview, and the exporter has to turn it back into a disk path to inline the bytes. That is the image's reason to change.

A link is the opposite situation: the href is the author's text. There is no reason for a document's link to name a webview-internal asset URL, and accepting one turns the URL scheme — the one thing a reader uses to judge where a link goes — into a lie.

The module's own comment lists "the two rules this caller adds that an image does not need". It missed the rule this caller should subtract.

Neither #363, #409 nor #403 is wrong on its own. The deception is what they compose into.

The fix

Reject asset URLs before delegating — three lines, plus the reasoning at the call site so the next person reusing this resolver sees which clause is image-specific.

Tests

scripts/localFileLinks.test.ts gains one test covering both URL forms, uppercase variants, a Windows drive-letter payload, and — asserted so a future loosening of the host pattern cannot pass this file — the asset.localhost.evil.test lookalike. Plus two assertions that ordinary links still resolve, so the guard cannot over-fire.

Implementation reverted, test kept 1 redan asset URL is never treated as a link to a local file
Final 12 / 12 in that file
npm run check   435 files, 0 errors
npm test        501 / 501

Not covered

  • Image src handling is unchanged and should be. An <img> pointing at an asset URL is exactly what the webview produces for a local image; only the link path rejects it.
  • The same composition question applies to any future caller of resolveExportImagePath: it answers "what disk path does this webview-side reference name", which is not the same question as "is this something the author asked to open".

🤖 Generated with Claude Code

… asset URL

`resolveLocalFileLinkPath` delegates to `resolveExportImagePath`, which
accepts `asset://localhost/…` and `http://asset.localhost/…` - the shape a
local image's `src` takes inside the webview, which the exporter has to
turn back into a disk path to inline the bytes.

A link is the opposite situation: the href is the author's own text. So
`[report](http://asset.localhost/Users/me/.ssh/id_rsa)` reads as a remote
address in the link text and the status bar, resolves to
`/Users/me/.ssh/id_rsa`, and is handed to the OS default handler - now
reachable, since the opener path scope was opened in #403.

Reusing the image resolver was right; inheriting that one clause of it
was not. The link resolver rejects asset URLs before delegating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao merged commit 68aec0a into master Aug 3, 2026
4 checks passed
@PathGao
PathGao deleted the fix/asset-url-link-deception branch August 3, 2026 07:09
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