Skip to content

fix(viewer): complete exhibit markdown rendering (styling, in-block scroll, link-scheme XSS guard) (#32) - #45

Merged
EYH0602 merged 5 commits into
mainfrom
feat/exhibit-markdown-rendering
Jul 20, 2026
Merged

fix(viewer): complete exhibit markdown rendering (styling, in-block scroll, link-scheme XSS guard) (#32)#45
EYH0602 merged 5 commits into
mainfrom
feat/exhibit-markdown-rendering

Conversation

@EYH0602

@EYH0602 EYH0602 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Completes the CSS/responsive layer and closes a residual XSS vector in the
client-side exhibit-body markdown renderer from #32. The initial #32 merge
shipped the <table> markup but not the styling/scroll layer, and neutralized
raw HTML but not markdown's own link/image sinks.

Two commits on this branch:

  1. Styling + in-block scroll — rendered exhibit tables now get the markdown
    data-table styling (borders, cell padding, header shading) and sit in an
    .exhibit-body { overflow-x:auto } container so wide tables scroll inside
    their block instead of overflowing the 70ch detail column. Corrects the stale
    RESULT-block comment. (Acceptance criteria Add real-ARA no-panic regression coverage (vendored subset + opt-in submodule sweep) #3, Real-ARA no-panic regression coverage (#3) #4.)
  2. Link/image scheme guardis_safe_url allowlists http/https/mailto
    (plus relative URLs) and rewrites any other scheme (javascript:, data:,
    vbscript:, …) to #. Whitespace/control chars are stripped first (the
    <...> destination form can deliver a tab into the parsed scheme) and entity
    encodings (&colon;, &#58;) are decoded by pulldown-cmark before the guard
    runs. Both behaviors were verified empirically and are locked in by tests.

Why it matters

ara serve can render a downloaded artifact, so exhibit bodies are not
always self-authored. Without the guard, a hostile evidence/tables/*.md with
[click](javascript:...) becomes click-to-execute in the viewer origin.

Tests

  • 6 new markdown.rs unit tests (js link, data image, control-char <...>
    evasion, entity-encoded scheme, safe/relative links preserved, is_safe_url
    classification).
  • 2 new web.rs tests: table renders inside .exhibit-body; empty body renders
    the chip but no body container.
  • cargo test -p ara-core -p ara-viewer --lib → 278 passed. Web tests compile
    clean for wasm32; the headless-Chrome run is exercised by the viewer-web-test
    CI job.
  • Bundle-size gate: 684 KB / 229 KB brotli (65% / 64% of budget).
  • Embedded viewer bundle regenerated; embed-viewer.sh --check green.

Version bumped to 0.1.11; CHANGELOG updated (Fixed + Security).

Closes #32.

EYH0602 added 4 commits July 20, 2026 10:13
Add a client-side markdown renderer for exhibit bodies (figures/tables)
in the viewer's RESULT block, using pulldown-cmark with the GFM tables +
strikethrough extensions. Raw HTML in a body is neutralised (re-emitted
as escaped text) so the rendered string is safe to mount via Leptos
`inner_html`. Math ($…$) stays literal, consistent with D3.

Plumb `Exhibit.body` (already carried by ara-core) through `ExhibitView`
and emit the rendered HTML under the evidence chips.

Bundle-size spike (wasm-release, wired so it is not DCE'd) passes the CI
gate: +160 KB uncompressed / +56 KB brotli, landing at 65% / 64% of the
1 MB / 350 KB budgets. Includes the plan doc with the measured numbers.

Remaining: responsive .exhibit-body scroll CSS, web tests, embed regen,
version bump + changelog.
The initial #32 render shipped the exhibit-body <table> markup but not the
CSS/responsive layer (plan steps 5-7), so exhibit tables were unstyled and
could overflow the 70ch detail column. Add the .exhibit-body scroll container
and share the markdown data-table styling with the rendered tables, correct
the stale RESULT-block comment, and add a web test asserting the table renders
inside the scroll wrapper. Bump to 0.1.11, roll the changelog, and regenerate
the embedded viewer bundle.
…down (#32)

The client-side exhibit-body renderer escaped raw HTML but passed markdown
link/image destinations to push_html unfiltered, so a downloaded artifact
could smuggle a clickable [x](javascript:...) or ![x](data:...) sink through
inner_html. Add an is_safe_url scheme allowlist (http/https/mailto + relative)
and rewrite unsafe Tag::Link/Tag::Image destinations to "#". Whitespace/control
chars are stripped first (the <...> dest form delivers a tab into the parsed
scheme) and entity encodings are decoded by pulldown-cmark before the guard
runs, both covered by regression tests. Also add a blank-body web test and
regenerate the embedded viewer bundle.

@fenfenai fenfenai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Reviewed 16 files (~620 additions) across bug-detection, error-handling, type-design, test-coverage, comment-quality, and guidelines-compliance aspects. The core change is a client-side GFM markdown renderer for exhibit bodies with an XSS link/image-scheme guard, plus the CSS/scroll layer that #32 left unshipped.

Verdict: Approve. No findings at confidence ≥75.

What I verified empirically (on the branch)

  • cargo test -p ara-viewer --lib markdown9 passed.
  • Adversarially probed the guard beyond the included tests — all neutralised:
    • Autolink <javascript:alert(1)>href="#"
    • Reference-style link ([x][r] / [r]: javascript:…) → href="#"
    • Raw HTML inside a table cell (<img onerror=…>) → escaped to text
    • Newline-in-scheme → link doesn't parse, rendered as literal text
  • is_safe_url's first-colon + /,?,#-in-scheme heuristic correctly classifies relative URLs (?q=a:b, #anchor, ./x) as safe.
  • Embedded bundle is consistent: index.html asset hashes (3b9263f1… js/wasm, e6588b2e… css) match the files on disk; viewer.source-hash present.
  • Version bumped 0.1.10 → 0.1.11; CHANGELOG has both Fixed and Security entries; pulldown-cmark pulled with default-features = false.

Strengths

  • The Event::Html | Event::InlineHtml → Event::Text re-emission closes raw HTML, and the scheme guard covers all Tag::Link/Tag::Image start events (inline, reference, and autolink forms alike) — no bypass found.
  • Control-char stripping in is_safe_url matches browser URL-parsing behavior (ASCII tab/newline/controls), and the tests lock in the non-obvious cases (<…> tab evasion, &colon;/&#58; entity decoding).
  • Math extension deliberately left off so $…$ stays inert — consistent with the existing latex_view posture, and documented.
  • Empty-body guard (!body.trim().is_empty()) is tested to skip the container.

Minor note (below threshold, non-blocking)

  • The allowlist rewrites all data: URLs to #, including data:image/*. That's a defensible security posture (blocks data:text/html), but a figure exhibit whose markdown embeds a legitimate base64 inline image would silently render a broken src="#". Exhibit figures typically reference image files by relative path, so this is unlikely to bite — just flagging in case inline data-image figures are expected. If they are, consider allowing data:image/* specifically.

@EYH0602

EYH0602 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

CC @AmberLJC demo

markdown.mp4

@EYH0602
EYH0602 merged commit e0bd491 into main Jul 20, 2026
15 checks passed
@EYH0602
EYH0602 deleted the feat/exhibit-markdown-rendering branch July 20, 2026 18:40
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.

RESULT: client-side markdown rendering of exhibit bodies (D4, hub-parity)

2 participants