Skip to content

fix(links): stop mangling percent-encoding when opening relative + external links - #288

Merged
mtskf merged 7 commits into
mainfrom
fix/link-open-percent-encoding
Jul 29, 2026
Merged

fix(links): stop mangling percent-encoding when opening relative + external links#288
mtskf merged 7 commits into
mainfrom
fix/link-open-percent-encoding

Conversation

@mtskf

@mtskf mtskf commented Jul 29, 2026

Copy link
Copy Markdown
Owner

What & why

Link opening mangled percent-encoding in both directions. Two localized host-side fixes:

(a) Relative links — handle-open-link.ts
[notes](my%20notes.md) (the standard form VS Code preview / GitHub / Obsidian emit) resolved to a literal %20-named file and failed to open. The host now percent-decodes the destination once before the Uri.joinPath resolve. Security is preserved: the #fragment is split on the encoded string first (a literal # in a filename is %23), decoding falls back to the raw form on a malformed escape (50%off.md), and every structural guard plus the workspace/containment check runs on the decoded, resolved target — so a decoded ../ traversal (..%2f..%2f…) resolves outside scope and is rejected. decodeURIComponent is all-or-nothing, so a path mixing valid + invalid escapes falls back to raw (not-found toast, no security impact).

(b) External links — build-external-uri.ts (new) + quoll-editor-panel.ts
env.openExternal(Uri.parse(url)) decoded %2F/ at parse time (unrecoverable) — silently rewriting GitLab-style API paths and +-bearing queries. The URL is now split with the WHATWG URL parser (which preserves percent-encoding in path/query/fragment, incl. userinfo) and rebuilt via Uri.from({…}). VS Code's OpenerService delivers it as encodeURI(uri.toString(true)) — skipEncoding, which leaves %2F/+ intact — so the destination reaches the browser byte-exact for those chars. Malformed URLs the WHATWG parser rejects fall back to the prior Uri.parse path.

Tests

  • Unit: decode-opens-space-file, sub-dir decode, malformed-escape fallback, decoded-traversal-rejected (handle-open-link.test.ts); %2F/+/%20/mailto/userinfo/password-only/null split (build-external-uri.test.ts).
  • E2E (real vscode.Uri): my%20notes.md resolves the space-named target and ..%2f… stays rejected under real Uri.joinPath; the external byte-exact %2F++ case captured through the real production handoff (the openExternalOverride seam now receives the built Uri).
  • Gate: pnpm compile + pnpm test:unit (4081) + lint + pnpm test:e2e (100) all green.

Notes

  • build-external-uri.ts added to the new URL( security choke-point allowlist (required lockstep).
  • The external byte-exactness rests on VS Code's encodeURI(toString(true)) opener behaviour (read from 1.130.0 source; a long-stable trait, documented in-code with a re-verify note).

@mtskf
mtskf force-pushed the fix/link-open-percent-encoding branch from 5758f37 to c0bf0a9 Compare July 29, 2026 03:33
mtskf added 2 commits July 29, 2026 13:56
…mment

Adds a regression test proving the post-decode isAllowedUrl re-check in
handleOpenLink actually gates something (removing it turns the test red),
a mixed-valid/invalid-escape fallback test, and an e2e test proving
buildExternalUri's Uri.parse fallback doesn't silently kill the
open-external flow when the WHATWG parser rejects the href. Also
corrects two stale comments in handle-open-external.ts that still
described the pre-buildExternalUri `Uri.parse(url)` closure.
@mtskf
mtskf merged commit 6d9d6ba into main Jul 29, 2026
1 check passed
@mtskf
mtskf deleted the fix/link-open-percent-encoding branch July 29, 2026 04:13
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