From 4d70587fd4706a513da2bd64bdeff267110b95f4 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 30 Oct 2026 23:10:05 -0500 Subject: [PATCH] fix(render): prevent delimiter adjacency breakouts --- CHANGELOG.md | 4 +- .../render_ir/footnote-link-adjacency.bin | Bin 0 -> 115 bytes .../render_ir/repeated-bold-inline-block.bin | Bin 0 -> 76 bytes crates/markkit/src/render/markdown.rs | 29 ++++++-- .../markkit/tests/fixtures/html/malformed.md | 2 +- crates/markkit/tests/output_safety.rs | 67 ++++++++++++++++++ knowledge/foundations/ir.md | 4 ++ knowledge/log.md | 2 +- knowledge/security/fuzzing.md | 5 +- knowledge/security/threat-model.md | 5 +- 10 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 crates/markkit/fuzz/seeds/render_ir/footnote-link-adjacency.bin create mode 100644 crates/markkit/fuzz/seeds/render_ir/repeated-bold-inline-block.bin diff --git a/CHANGELOG.md b/CHANGELOG.md index d5743df..1f7efdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). below each term, preserving code blocks without requiring a definition-list extension. Nested lists beginning with an empty item are separated from preceding text. Hard breaks inside headings and link labels are flattened so they cannot close the - container before later inline code. + container before later inline code. Redundant nested bold, italic and + strikethrough wrappers no longer emit delimiters that close their outer span, + and adjacent bracketed nodes cannot combine with following text into a link. - PDF stream guards honor escaped dictionary keys and bound compressed prefixes before image filters, including when OCR is disabled. diff --git a/crates/markkit/fuzz/seeds/render_ir/footnote-link-adjacency.bin b/crates/markkit/fuzz/seeds/render_ir/footnote-link-adjacency.bin new file mode 100644 index 0000000000000000000000000000000000000000..ef240c1eb048f8f8ccba80b38e1bb1ba0519858e GIT binary patch literal 115 zcmZ>F2Lc6E_u7j__uXsH7~N;M`Jl1sy6OGH2WscfuV-iY|KH|f08&#iumAu6 literal 0 HcmV?d00001 diff --git a/crates/markkit/fuzz/seeds/render_ir/repeated-bold-inline-block.bin b/crates/markkit/fuzz/seeds/render_ir/repeated-bold-inline-block.bin new file mode 100644 index 0000000000000000000000000000000000000000..044025a8321b21fb3900ea661be6b78346cfec9d GIT binary patch literal 76 zcmcavSF6hP|Nr58@$=`;uV-iY|KH| self.render_wrapped("**", children), - Node::Italic { children } => self.render_wrapped("*", children), - Node::Strikethrough { children } => self.render_wrapped("~~", children), + Node::Bold { children } => self.render_wrapped("**", WRAP_BOLD, children), + Node::Italic { children } => self.render_wrapped("*", WRAP_ITALIC, children), + Node::Strikethrough { children } => { + self.render_wrapped("~~", WRAP_STRIKETHROUGH, children); + } Node::InlineCode(code) => { escape::push_code_span(&mut self.buf, code, self.in_table); self.line_start = false; @@ -553,11 +568,17 @@ impl MarkdownRenderer { } } - fn render_wrapped(&mut self, delim: &str, children: &[Node]) { + fn render_wrapped(&mut self, delim: &str, wrapper: u8, children: &[Node]) { + if self.active_wrappers & wrapper != 0 { + self.render_inline(children); + return; + } + self.active_wrappers |= wrapper; self.buf.push_str(delim); self.line_start = false; self.render_inline(children); self.buf.push_str(delim); + self.active_wrappers &= !wrapper; } /// Render a block node as single-line inline content. diff --git a/crates/markkit/tests/fixtures/html/malformed.md b/crates/markkit/tests/fixtures/html/malformed.md index 2161df6..ee23a3a 100644 --- a/crates/markkit/tests/fixtures/html/malformed.md +++ b/crates/markkit/tests/fixtures/html/malformed.md @@ -4,4 +4,4 @@ Next paragraph Mismatched **bold with no close** -**Entities: & \< > " ' Nested *italic **and bold***** crossed** ** +**Entities: & \< > " ' Nested *italic and bold* crossed ** diff --git a/crates/markkit/tests/output_safety.rs b/crates/markkit/tests/output_safety.rs index d1800c2..aabcbc8 100644 --- a/crates/markkit/tests/output_safety.rs +++ b/crates/markkit/tests/output_safety.rs @@ -617,6 +617,73 @@ fn blockquote_heading_inline_blocks_cannot_expose_html() { assert!(visible_text(&md).contains("<< ` diff --git a/knowledge/log.md b/knowledge/log.md index ecadd9f..c5e6d1e 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -2,7 +2,7 @@ ## 2026-10-31 -* **Container fence safety**: task checkboxes render on their own line; definition bodies use ordinary GFM list items below their term. This prevents markers from consuming code fences and exposing raw HTML, including empty leading paragraphs and nested definitions. A nested list with an empty first item also requires a blank line before it; the renderer checks actual output once, within the remaining output budget. Headings, link labels and block nodes flattened into inline content now collapse hard breaks to spaces, preventing a break from closing the container before later inline code. Exact nightly crash seeds and content/grouping regressions cover these paths. See [Threat Model](security/threat-model.md) T1 and [IR Specification](foundations/ir.md). +* **Container fence safety**: task checkboxes render on their own line; definition bodies use ordinary GFM list items below their term. This prevents markers from consuming code fences and exposing raw HTML, including empty leading paragraphs and nested definitions. A nested list with an empty first item also requires a blank line before it; the renderer checks actual output once, within the remaining output budget. Headings, link labels and block nodes flattened into inline content collapse hard breaks to spaces, preventing a break from closing the container before later inline code. Redundant nested bold, italic and strikethrough wrappers render once so an inner delimiter cannot close its outer span. A bracketed node followed by text beginning with `(` is separated so it cannot form an unintended inline link. Exact nightly crash seeds and content/grouping regressions cover these paths. See [Threat Model](security/threat-model.md) T1 and [IR Specification](foundations/ir.md). ## 2026-10-28 diff --git a/knowledge/security/fuzzing.md b/knowledge/security/fuzzing.md index b40571b..fffc631 100644 --- a/knowledge/security/fuzzing.md +++ b/knowledge/security/fuzzing.md @@ -82,7 +82,10 @@ the Actions cache; crashes upload as `fuzz-crashes-` artifacts. links, adjacent code spans merging, and code fences consumed as paragraph text after task-checkbox, nested definition markers, or a nested ordered list with an empty first item. A hard break inside a link label nested in a heading - also closed both containers before later inline code. Exact crash seeds for + also closed both containers before later inline code; repeating a bold wrapper + inside bold closed the outer span with the same result. A footnote reference + followed by text beginning with `(` could also combine into an inline link and + consume later delimiters. Exact crash seeds for these cases are retained in `fuzz/seeds/render_ir/` and `fuzz/seeds/markdown/`. - XLS (calamine 0.37): unchecked reads in FILEPASS, CODEPAGE, DATEMODE, BOF, BOUNDSHEET, LBL (incl. defined-name tokens), EXTERNSHEET, SST (cRun / diff --git a/knowledge/security/threat-model.md b/knowledge/security/threat-model.md index 0ca9ce8..45a4b02 100644 --- a/knowledge/security/threat-model.md +++ b/knowledge/security/threat-model.md @@ -58,8 +58,9 @@ that renders as headings, links, code fences or tables). inside headings and link labels; list item bodies are indented by the marker width (`1. ` = 3, `10. ` = 4); a block always starts on its own line after a blank line; hard breaks never produce a blank line; - nested links render as text; empty destinations are written `<>`; link titles - escape `"`, `\` and `<`. + nested links render as text; redundant nested emphasis wrappers render once; + adjacent bracketed nodes and opening parentheses are separated; empty + destinations are written `<>`; link titles escape `"`, `\` and `<`. - Task checkboxes occupy their own line before the item body so they cannot turn a leading code fence into paragraph text. Definition lists render as a term paragraph and ordinary GFM list items, keeping nested blocks safe