Skip to content

fix(render-html): Harden HTML embedding boundary and metadata escaping#163

Merged
mhiro2 merged 3 commits into
mainfrom
harden/render-html-embedding-boundary
May 14, 2026
Merged

fix(render-html): Harden HTML embedding boundary and metadata escaping#163
mhiro2 merged 3 commits into
mainfrom
harden/render-html-embedding-boundary

Conversation

@mhiro2
Copy link
Copy Markdown
Owner

@mhiro2 mhiro2 commented May 14, 2026

Summary

  • Make the </script> neutralization in embedded JSON metadata ASCII case-insensitive so </SCRIPT> and </Script foo> cannot terminate the script block in HTML parsers.
  • Switch <!-- / --> neutralization to JSON Unicode escapes (< / >) so the embedded metadata stays valid JSON and JSON.parse recovers the original characters losslessly.
  • Introduce a typed SvgArtifact newtype produced by the SVG renderer and consumed by render_html(_with_overlay), making it impossible to pass arbitrary &str into the HTML embedding boundary by accident.

Changes

  • cb7fe4a : fix(render-html): escape script end tag case-insensitively
    • Rewrite escape_json_for_script as a byte scanner that detects </script case-insensitively, preserves the original casing, and inserts only a backslash before the slash.
    • Add a matches_ascii_ci helper and tests for uppercase/mixed-case end tags and Unicode-bearing JSON.
  • f97b2a5 : refactor(render-html): require typed SvgArtifact for HTML embedding
    • Add SvgArtifact newtype to relune-render-svg; render_svg/render_svg_with_overlay now return Result<SvgArtifact, _> with an explicit from_trusted escape hatch.
    • Re-export SvgArtifact from relune-render-html and change render_html/render_html_with_overlay to take &SvgArtifact, threading the typed boundary through the app use cases.
  • d61f422 : fix(render-html): emit JSON-valid Unicode escapes for HTML comment markers
    • Replace the previous <\!-- / -\-> output (which is not a valid JSON escape) with <!-- / --> so browser-side JSON.parse of the embedded metadata no longer fails on payloads containing HTML comment markers.
    • Extend the test to assert the output round-trips through serde_json::from_str back to the original characters.

mhiro2 added 3 commits May 14, 2026 22:25
HTML parsers terminate <script> blocks on any case variant of the
</script tag, including </SCRIPT>, </Script>, or </script foo>. The
previous escape only matched the exact lowercase </script> string,
which let metadata containing those variants escape the JSON island
and execute as markup. Match the </script prefix case-insensitively
and preserve the original casing when emitting the inert backslash
sequence, and cover the variants with regression tests.
render_html previously accepted any &str as the SVG payload, so the
crate boundary allowed arbitrary <script> blocks, event handlers, or
external resource references to be wrapped in the trusted HTML
document. Introduce SvgArtifact in relune-render-svg as the typed
output of render_svg / render_svg_with_overlay, and require
&SvgArtifact in render_html / render_html_with_overlay so the only
zero-friction path is through the SVG renderer that already escapes
user-derived content. SvgArtifact::from_trusted remains for callers
that need to wrap an externally produced SVG and accept the same
escaping contract.
…rkers

Previously `<!--` / `-->` were rewritten as `<\!--` / `-\->`, which are
not valid JSON escape sequences. Browser-side `JSON.parse` of the
embedded metadata would therefore fail on any metadata containing those
markers, leaving search/highlight/etc. with `null`.

Use `<!--` and `-->` instead. The HTML parser still cannot see
a literal `<!--` or `-->` in the script body, but `JSON.parse` losslessly
recovers the original characters.
@mhiro2 mhiro2 added the bug Something isn't working label May 14, 2026
@mhiro2 mhiro2 self-assigned this May 14, 2026
@github-actions
Copy link
Copy Markdown

Code Metrics Report

main (b9a997f) #163 (288f5d9) +/-
Coverage 94.4% 94.4% +0.0%
Test Execution Time 2m12s 1m46s -26s
Details
  |                     | main (b9a997f) | #163 (288f5d9) |  +/-  |
  |---------------------|----------------|----------------|-------|
+ | Coverage            |          94.4% |          94.4% | +0.0% |
  |   Files             |            100 |            100 |     0 |
  |   Lines             |          36742 |          36825 |   +83 |
+ |   Covered           |          34704 |          34784 |   +80 |
+ | Test Execution Time |          2m12s |          1m46s |  -26s |

Code coverage of files in pull request scope (94.4% → 94.4%)

Files Coverage +/- Status
crates/relune-app/src/usecases/diff.rs 88.5% 0.0% modified
crates/relune-app/src/usecases/render.rs 96.8% 0.0% modified
crates/relune-render-html/src/html.rs 99.8% +0.0% modified
crates/relune-render-html/src/lib.rs 100.0% 0.0% modified
crates/relune-render-svg/src/lib.rs 97.8% -0.2% modified

Reported by octocov

@github-actions
Copy link
Copy Markdown

Schema review

Tip

✅ No risk findings — schema changes look safe to merge.

@mhiro2 mhiro2 merged commit c9be562 into main May 14, 2026
6 checks passed
@mhiro2 mhiro2 deleted the harden/render-html-embedding-boundary branch May 14, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant