Repository navigation
fix(render): escape markdown output and allowlist URL schemes - #23
Merged
Merged
Conversation
chaliy
force-pushed
the
claude/project-thread-ulbh3z
branch
2 times, most recently
from
October 27, 2026 01:04
4162ad7 to
26258a7
Compare
Document text could emit raw HTML, links, emphasis, headings and code-fence breakouts, and javascript: links passed through unchanged (threat model T1). - New render/escape.rs: text escaping (inline syntax, line-start block markers), code fences/spans longer than inner backtick runs, link and image scheme allowlists, percent-encoded destinations, escaped titles - Structure cannot leave its container: blocks in inline context flattened, list bodies indented by marker width, blocks always start a new line, hard breaks never make blank lines, nested links render as text - DOCX and spreadsheet parsers no longer pre-escape table pipes; the IR holds raw text and the renderer escapes in table context - output_safety tests with a pulldown-cmark oracle and proptests (S14); cases found by a fuzzing pass against that oracle are regression tests
chaliy
force-pushed
the
claude/project-thread-ulbh3z
branch
from
October 27, 2026 01:22
26258a7 to
a8152a0
Compare
This was referenced Oct 27, 2026
chaliy
added a commit
that referenced
this pull request
Oct 27, 2026
Re-run after #23 (markdown escaping): product_page now fails contains "4.6 out of 5 stars" because the renderer emits 4\.6, which is not a list marker. Recorded as an open gap.
This was referenced Oct 27, 2026
chaliy
added a commit
that referenced
this pull request
Oct 27, 2026
<!-- claude-projects-attribution: {"github_login":"chaliy"} -->
_Requested by **Михайло** · [project
thread](https://claude.ai/code/project/chan_01MUtksmx61ypcr6wBFEWsbM?thread=cmsg_01MUtksmx61ypcr6wBFEWsbMDLeuybnBNqgVL9B9kMmARU)_
## What changed
Text that only looks like a block marker at the start of a line is no
longer backslash-escaped. `#`, `+` and ordered markers (`1.`, `1)`) are
escaped only when followed by whitespace or the end of the text run
(where the next node could supply the space). `>`, `-` and `=` stay
always escaped (quotes, thematic breaks, setext underlines).
## Why
Regression from #23 found by the eval: `4.6 out of 5 stars` rendered as
`4\.6 out of 5 stars`. The backslash is noise for text consumers, and
`4.6` can never open a list.
## Before / After
| Input text | Before | After |
|---|---|---|
| `4.6 out of 5 stars` | `4\.6 out of 5 stars` | `4.6 out of 5 stars` |
| `#hashtag` | `\#hashtag` | `#hashtag` |
| `+1 for this` | `\+1 for this` | `+1 for this` |
| `1. li` | `1\. li` | `1\. li` (unchanged) |
| `#` then node ` h` | `\# h` | `\# h` (unchanged) |
New tests in `tests/output_safety.rs`:
`line_start_marker_lookalikes_render_verbatim` (failed before the fix)
and `line_start_marker_at_run_end_stays_escaped`. The pulldown-cmark
oracle still sees exactly one paragraph for every case. `cargo test
--workspace` passes.
## Risk
- Low
- Narrows escaping only where CommonMark cannot open a block; run-end
case stays escaped so split text nodes cannot combine into a marker.
## Checklist
- [x] Tests added or updated
- [x] Backward compatibility considered (output-only change; CHANGELOG
Fixed, threat model T1 and eval gap note updated)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FWdY28wwTahFw3u9xhDBAv)_
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Михайло · project thread
What changed
Markdown output is now safe to render, for every input format. The PR is rebased onto current
main, so it covers IR v2 (footnotes, definition lists, figures, task items, table spans), math (#34), transforms (#36), relative URL resolution (#31) and the limits from #27. #27's output and deadline checks are kept in the reworked renderer.Text renders verbatim. These characters are escaped:
*,_(at word boundaries),[,], the less-than sign,~,|and entity-like&;#,>,-,+,=,1..Document text can no longer produce raw HTML, links, emphasis, headings, lists or tables.
URL scheme allowlist. Links keep
http,https,mailto,tel,ftpand relative URLs. Images keephttp,https, relative URLs and rasterdata:image/*. Any other scheme is dropped and its link text or alt text is kept:javascript:,vbscript:,data:text/html,file:. Scheme detection ignores whitespace and control characters and reads the whole scheme.Destinations and titles. Parentheses, angle brackets, backslash and spaces are percent-encoded. Entity-like
&is escaped, sojavascript:cannot decode into a scheme. Empty destinations are written<>. Titles escape quotes, backslash and less-than.Code. Fences and spans are longer than any backtick run inside them, and adjacent spans cannot merge their fences. The code-block language keeps only its first word.
Math renders as code. TeX comes from page markup, and plain GFM has no math syntax, so
$tex$let an annotation containing an img tag or a link through. Math now uses GitHub's forms:$`tex`$inline and a```mathfence for display math. GitHub renders both as math. Every other renderer shows them as inert code.Structure stays in its container.
1.items now nests correctly.Raw text in the IR. The DOCX and spreadsheet parsers stop pre-escaping table pipes, so the JSON IR holds raw text. The renderer escapes pipes in table context.
All escaping lives in the new
render/escape.rs, with the design decisions in its header. The new invariant is S17, since #27 took S14 to S16.Why
Threat model T1 promised "no HTML pass-through", but the renderer escaped nothing. On
main, a probe page showed four problems:javascript:links;preblock closed the fence;).A fuzzing pass against the new oracle then found eight more ways content could escape its container. The cargo-fuzz harness comes in a follow-up PR. Each finding is a regression test here.
Before / After
Input (HTML) is a paragraph containing
# not heading *x*, followed by:javascript:alert(1),http://x/a)b,pre/codeblock holding a triple-backtick line, thenevil, then another triple-backtick line.Before:
);evilended up outside the code block.After:
The new
tests/output_safety.rshas 39 tests, 4 of them for math. 22 of them failed on the oldmain. A pulldown-cmark (GFM) oracle checks four things:Proptests cover arbitrary text, URLs and code. #27's
limitstests pass in debug and release, including the deep-nesting tests under a small stack.just pre-prpasses locally exceptcargo deny, which is not installed here; the CI Audit job runs it. New dev-dependency:pulldown-cmark(MIT), used in tests only.Risk
<>.$`tex`$and```math.javascript:pass-through test, nowabsolute_dangerous_schemes_are_not_resolved. Base resolution still never rewrites the URL, and the allowlist then drops it.Checklist
🤖 Generated with Claude Code