Skip to content

fix(charts): confine untrusted chart options to canvas render paths - #6998

Merged
icecrasher321 merged 1 commit into
stagingfrom
fix/chart-preview-xss
Aug 23, 2026
Merged

fix(charts): confine untrusted chart options to canvas render paths#6998
icecrasher321 merged 1 commit into
stagingfrom
fix/chart-preview-xss

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Summary

  • parseChartSpec forwarded a .chart document's option to setOption unchanged. A .chart file is untrusted input — any workspace member authors one, and a public share link renders it to anonymous visitors on the app origin — so a string tooltip.formatter reached ECharts' innerHTML sink as stored XSS.
  • ECharts draws through canvas with two exceptions: a tooltip left in its default renderMode: 'html' assigns its content to innerHTML, and a string formatter is that content's template verbatim (only substituted values are escaped); toolbox assigns dataView.lang to innerHTML and fills a saveAsImage popup with document.write. Both are now closed at the parse boundary, so the document has no DOM sink at all — rather than filtering the values that flow through one.
  • The walk is deep because a top-level fix is provably incomplete: baseOption, media[].option and timeline options[] each carry their own tooltip, a tooltip declared only under media still instantiates the component in HTML mode, and a media entry can override a top-level renderMode.
  • Escaped the series name in the pptx chart renderer's bubble tooltip — same sink, different door. It comes from the uploaded document and is interpolated into a hand-built innerHTML string; every other tooltip there leaves the markup to ECharts, which escapes it. Used encodeHTML rather than the render mode because Sim owns that formatter and only the values are untrusted.
  • Deliberately did not restrict contentType on file creation: resolvePreviewType falls back to the .chart extension, so it closes nothing and would reject legitimate types.

renderMode: 'richText' is ECharts' own documented injection-safe mode. One behavior note: HTML inside an author's formatter string (<br/>, <b>) now renders as literal text. Templates ({b}: {c}), default tooltips and markers are unaffected — ECharts builds those per-mode.

Side effect: a document nested deeply enough to exhaust the stack already crashed the render (structuredClone in buildChartRenderOption throws on it). It's now rejected at parse, so it shows the document's error card instead of failing inside the render.

Type of Change

  • Bug fix

Testing

  • 20 new tests in lib/charts/spec.test.ts, including two that render through real ECharts in SSR mode and assert the resolved model — a version bump that breaks the renderMode assumption fails loudly instead of silently reopening the hole
  • bun run test lib/charts lib/pptx-renderer app/workspace/[workspaceId]/files — 801 passing
  • type-check, lint, check:block-registry and check:audits (33 audits, incl. check:api-validation:strict) all clean

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

A `.chart` document is untrusted input — any workspace member authors one,
and a public share link renders it to anonymous visitors on the app origin —
but `parseChartSpec` forwarded `option` to `setOption` unchanged.

ECharts draws through canvas with two exceptions: a `tooltip` left in its
default `renderMode: 'html'` assigns its content to `innerHTML`, and a string
`formatter` is that content's template verbatim (only substituted values are
escaped), while `toolbox` assigns `dataView.lang` to `innerHTML` and fills a
`saveAsImage` popup with `document.write`. Force the render mode and drop the
toolbox so the document has no DOM sink, rather than filtering the values
that flow through one.

The walk is deep: `baseOption`, `media[].option` and timeline `options[]`
each carry their own tooltip, a tooltip declared only under `media` still
instantiates the component in HTML mode, and a `media` entry can override a
top-level `renderMode`.

Also escape the series name in the pptx chart renderer's bubble tooltip — it
comes from the uploaded document and is interpolated into a hand-built
`innerHTML` string, where ECharts escapes only the markup it builds itself.
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 22, 2026 11:50pm

Request Review

@cursor

cursor Bot commented Aug 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Security-sensitive XSS fix on untrusted chart input that can render on the app origin for anonymous visitors. Incomplete confinement or an ECharts renderMode change could reopen injection.

Overview
Closes stored XSS in untrusted .chart documents (workspace-authored, also rendered on public share links) by sanitizing ECharts options at parse time instead of forwarding them to setOption unchanged.

parseChartSpec now walks the option tree: every tooltip is forced to renderMode: 'richText' (canvas, not innerHTML), and toolbox is stripped at all nesting levels (baseOption, media, timeline options, series). Dataset rows are left alone. Over-deep documents that would stack-overflow the walk are rejected with an error card.

Separately, the PPTX bubble-chart tooltip HTML-encodes the uploaded series name before interpolating it into a hand-built formatter.

HTML in author formatter strings now shows as literal text. Tests pin both the parse walk and the resolved ECharts model in SSR.

Reviewed by Cursor Bugbot for commit b878a46. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR confines untrusted .chart options to non-DOM ECharts rendering paths and escapes uploaded bubble-series names before inserting them into HTML tooltips.

  • Recursively forces tooltip rendering to richText and removes toolbox configurations during chart-spec parsing.
  • Adds coverage for nested, responsive, timeline, array-valued, and excessively deep chart options.
  • HTML-encodes PPTX bubble-chart series names in the custom tooltip formatter.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code defect remaining after review.

The changed parser closes the identified untrusted tooltip and toolbox DOM paths, while the PPTX formatter now escapes its document-derived series name; the added tests exercise the important nested and resolved-model cases.

Important Files Changed

Filename Overview
apps/sim/lib/charts/spec.ts Adds parse-boundary traversal that forces rich-text tooltips, removes toolbox options, and converts traversal overflow into a chart-document validation error.
apps/sim/lib/charts/spec.test.ts Adds comprehensive unit and ECharts model tests covering nested confinement, toolbox removal, dataset preservation, and depth rejection.
apps/sim/lib/pptx-renderer/renderer/chart-renderer.ts Escapes the uploaded bubble-series name before interpolating it into the custom HTML tooltip.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Untrusted chart document] --> B[parseChartSpec]
  B --> C[Recursively confine options]
  C --> D[Force tooltip renderMode to richText]
  C --> E[Remove toolbox]
  D --> F[buildChartRenderOption]
  E --> F
  F --> G[ECharts canvas-safe rendering]
  H[Uploaded PPTX bubble series name] --> I[encodeHTML]
  I --> J[Custom HTML tooltip]
Loading

Reviews (1): Last reviewed commit: "fix(charts): confine untrusted chart opt..." | Re-trigger Greptile

@icecrasher321
icecrasher321 merged commit e406b8a into staging Aug 23, 2026
30 checks passed
@icecrasher321
icecrasher321 deleted the fix/chart-preview-xss branch August 23, 2026 00:06
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.

1 participant