Skip to content

fix(html): filter a document hyperlink through the scheme allowlist - #756

Merged
andiwand merged 1 commit into
mainfrom
fix/link-scheme-allowlist
Aug 29, 2026
Merged

fix(html): filter a document hyperlink through the scheme allowlist#756
andiwand merged 1 commit into
mainfrom
fix/link-scheme-allowlist

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Fixes #737.

translate_link wrote a link's target through escape_attribute and nothing else, so the scheme stayed live: a javascript: href in an odt, docx, pptx, xlsx or markdown reached the rendered page and ran script in whatever origin serves the generated html. Entity-obfuscated variants landed there too — [a](javascript:alert(1)) — because the numeric reference is resolved before the href is stored, so the filter has to run after that resolution, which this one does.

AGENTS.md already recorded this as the one place the html output's policy was inconsistent. It is now one policy:

  • is_safe_uri moves out of html/pdf_file.cpp into html/common.*, unchanged — the six navigable schemes (http, https, mailto, ftp, ftps, tel) plus scheme-less references, reading the scheme past embedded whitespace and control bytes so java\tscript: cannot slip through.
  • translate_link calls it, and a refused target loses its href rather than becoming # or plain text — the smallest change, and the link keeps its text and its styling.
  • pdf_file.cpp calls the same function from the same place it did before.

Link::href() on the element api is unaffected; this is only about what the renderer emits.

The allowlist was not widened for documents. file: was the candidate — the corpus has no file: hyperlink in it, and a file: href is exactly the one that matters in a webview serving generated html from a local origin, so keeping it out costs nothing and avoids re-opening the two-policy split.

Test

  • html_common.* — four unit tests over the lifted function: the navigable schemes, scheme-less references, the refused schemes, and whitespace/NUL inside a scheme.
  • html.a_link_the_page_must_not_navigate_to_loses_its_href / ..._keeps_its_href — end to end through the renderer on the issue's own repro, including the numeric and hex entity forms.
  • Full suite green. Reference output moved for three documents that genuinely carry javascript:void(0) links (physics.docx, its reflow variant, style-missing+image-1.odt); every other byte is identical, and the pins in test/data.cmake are advanced to the regenerated commits.

`translate_link` wrote a link's target through `escape_attribute` and
nothing else, so a `javascript:` href in an odt, docx, pptx, xlsx or
markdown reached the rendered page and ran script in whatever origin
serves it. Markdown is what made that cheap to reach: no container to
build, and the format most likely to be user-authored untrusted text.

The allowlist a PDF `/URI` action already went through moves from
`html/pdf_file.cpp` into `html/common.*` and both callers use it, which
leaves one policy instead of two. A refused target loses its `href` and
keeps its text and styling; `Link::href()` is untouched.

Fixes #737

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Re7MMYiM7fL58uELzKGy77
@andiwand
andiwand force-pushed the fix/link-scheme-allowlist branch from 5acebc0 to 8aeedd0 Compare August 29, 2026 08:01
@andiwand
andiwand enabled auto-merge (squash) August 29, 2026 08:03
@andiwand
andiwand merged commit efa9d83 into main Aug 29, 2026
36 checks passed
@andiwand
andiwand deleted the fix/link-scheme-allowlist branch August 29, 2026 08:15
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.

A document hyperlink keeps its scheme, so a javascript: href reaches the rendered page

1 participant