Security Threat Model: Apache ECharts (AppSheet Fork)

Asset Definition & Scope

  • Component: Apache ECharts — client-side JavaScript/TypeScript charting and data-visualization library (AppSheet fork)
  • Repository: Git-on-Borg (https://gnocchi-internal.googlesource.com/third_party/echarts, branch main). Mirrored as appsheet-third-party/echarts, which is the URL actually resolved by Nirvana/Content/package.json.
  • Upstream version: Apache ECharts 5.4.0 (upstream commit cb722aecad5b44476a5c2f4c343b699cfa834b0e, per METADATA; last_upgrade_date 2022-11-16). Declared in package.json and src/core/echarts.ts:version.
  • Runtime dependency: zrender pinned to a pre-release nightly (npm:zrender-nightly@^5.4.1-dev.20221104) rather than a stable release.
  • Scope: Browser-side rendering library bundled into the AppSheet web frontend (jeenee/Nirvana/Content). It renders bar, line, area, pie, doughnut, scatter, KPI and graph charts for (a) end-user-facing AppSheet application chart views and (b) the AppSheet Admin Console. Scope covers the TypeScript sources under src/ and the pre-built bundles checked into dist/, which are shipped to browsers.
  • Fork delta: The fork consists of a squashed upstream import plus four commits (4e53257, 1bd413c, 60d53ba, 967afa1), all of which modify src/component/tooltip/TooltipHTMLContent.ts (and regenerated dist/ bundles) to suppress Trusted Types enforcement violations. No upstream security patches have been backported.

Prioritization Signals

  • 1P OSS: No (Third-party Apache-licensed OSS maintained as an internal customized fork for Google AppSheet)
  • 1P Proprietary Shipped Software: Yes (Compiled and minified into the AppSheet frontend JavaScript bundles served to every AppSheet user's browser; dist/*.min.js artifacts are committed to this repository)
  • High-Risk Code Surface: Yes (Client-side rendering engine that interpolates caller-supplied strings into innerHTML, contains an intentionally pass-through Trusted Types policy added by the fork, performs dynamic code evaluation via new Function in GeoJSON parsing, and writes unvalidated CSS into inline style attributes)
  • Perimeter Exposure: Yes (Executes in the browsers of public Internet users of AppSheet applications and in the AppSheet Admin Console; AppSheet is a 1P service exposed to the public Internet and to customers)
  • Data Sensitivity: High (Renders customer business records — the row values, column display names, app names, and creator email addresses that flow into chart series, axis categories, labels, and tooltips are customer business data and PII, displayed client-side)
  • Untrusted Input Handling: Yes (Two distinct untrusted sources: chart configuration authored by AppSheet app creators — chart type, column display names, aggregations, titles — and record data entered by application end users, both of which reach label and tooltip rendering paths)
  • Business Value: ECharts is the sole charting engine behind AppSheet chart views and the Admin Console dashboards. A DOM-XSS defect in a tooltip or label sink executes attacker JavaScript in the AppSheet origin, enabling session theft, cross-tenant data access, and privilege escalation against app creators and Admin Console operators. The fork's deliberate weakening of Trusted Types removes a defense-in-depth control that would otherwise mitigate exactly this class of bug.

Scanning Harness Prompts

  1. Pass-through Trusted Types policies (fork-introduced, highest priority). TooltipHTMLContent.setContent creates Trusted Types policies named TooltipHTMLFix1, TooltipHTMLFix2, and TooltipHTMLFix3. TooltipHTMLFix2 is defined as createHTML: (string) => string — an identity function that launders arbitrary strings into TrustedHTML and assigns them to el.innerHTML. Treat this as a Trusted Types bypass, not a fix: it converts a browser-enforced XSS guardrail into a no-op while satisfying the violation reporter. Flag every string reaching this sink. Also flag that createPolicy is invoked on every setContent call (i.e. on every tooltip render) rather than once at module init — under a CSP trusted-types allow-list without allow-duplicates this throws a TypeError on the second call, and it is an unbounded policy-creation hot path.
  2. Tooltip formatter → innerHTML data flow. Trace TooltipView.prototype._showTooltipContent → tooltipContent.setContent → TooltipHTMLContent.setContent → el.innerHTML. Note the asymmetry: a string formatter has its {a}/{b}/{c} placeholders escaped via formatTpl(html, params, true) → encodeHTML, but the template shell itself is raw HTML; a function formatter return value is inserted with no escaping whatsoever. The default (no-formatter) markup path in src/component/tooltip/tooltipMarkup.ts does apply encodeHTML to names and values and is the safe baseline — verify no regression there.
  3. CSS injection via extraCssText. In TooltipHTMLContent.prototype.show, tooltipModel.get('extraCssText') is concatenated raw into el.style.cssText with no validation or delimiter escaping. Check for injection of additional CSS declarations, and for exfiltration primitives such as background:url(...) attribute selectors.
  4. Dynamic code evaluation in GeoJSON parsing. src/coord/geo/GeoJSONResource.ts:parseInput falls back to new Function('return (' + source + ');')() when JSON.parse is unavailable. Confirm this fallback is unreachable in supported browsers and that registerMap callers never pass attacker-controlled strings; any reach is direct arbitrary JavaScript execution.
  5. Unprotected innerHTML / document.write sinks outside the tooltip. The fork patched only the tooltip. Audit the following, none of which are wrapped in a Trusted Types policy and all of which will hard-fail or bypass under TT enforcement:
    • src/component/toolbox/feature/DataView.ts — header.innerHTML from lang[0] || model.get('title'), viewMain.innerHTML = htmlOrDom from the caller-supplied optionToContent callback, and closeButton.innerHTML / refreshButton.innerHTML from lang.
    • src/component/toolbox/feature/SaveAsImage.ts — tab.document.write(html) and doc.write(bstr) build markup by string concatenation, interpolating url and lang[0] into an <img src=... title=...> attribute context.
    • src/core/echarts.ts — getSvgRoot().innerHTML = content in the connected-charts SVG export path.
    • TooltipHTMLContent.setContent — arrowEl.innerHTML = arrow in the HTMLElement[] content branch is not routed through any policy, unlike the sibling string branch.
  6. Build-artifact divergence between src/ and dist/ (supply-chain integrity). The committed bundles are inconsistent with each other and with the sources — see the table under Privileged Actions. dist/echarts.simple.js / .simple.min.js contain none of the fork patches, and dist/echarts.esm.js / .esm.min.js are stale at the 4e53257 state (only the empty-clear TooltipHTMLFix1 policy; missing the TooltipHTMLFix2 string-content path). Verify which artifact AppSheet actually ships and whether dist/ can be reproduced from src/.
  7. Rich-text template token injection. Label formatter values that return ECharts rich-text markup ({styleName|text}) interpolate data values into the template. Unescaped {, }, or | characters in a data value can break out of the intended token and select a different rich style. This is canvas/SVG-rendered rather than an HTML sink, so treat it as a UI-spoofing and rendering-integrity issue rather than XSS.
  8. Missing upstream security patches. The fork has not been rebased since 2022-11-16. Enumerate Apache ECharts and zrender advisories affecting 5.4.0 and the zrender-nightly@5.4.1-dev.20221104 pre-release, and check whether each fix is absent here.

Entry Points and Untrusted Inputs

All entry points are library APIs invoked by AppSheet frontend code; the library performs no authentication of its own.

Entry PointTypeTrusted?Validation
echartsInstance.setOption(option) — the primary API; receives the full chart configPublic API / config objectNoNo schema validation or sanitization; option values are mapped onto component models and rendered
tooltip.formatter (function) — e.g. ChartView.tsx, bar_chart_builder.ts, scatter_chart_builder.tsCallback returning HTMLNoNone. Return value flows to el.innerHTML via the pass-through TT policy
tooltip.formatter (string template)HTML template with {a}/{b}/{c}NoPlaceholders escaped by formatTpl(..., encode=true) → encodeHTML; the surrounding template is raw HTML
Default tooltip markup (no formatter)Internal markup builderNotooltipMarkup.ts applies encodeHTML to series names and values — safe baseline
tooltip.extraCssTextRaw CSS stringNoNone. Concatenated directly into el.style.cssText
series.label.formatter / axisLabel.formatter / legend formatterString or callbackNoRendered as canvas/SVG text, not HTML; rich-text `{style
series[].name, series.data[].name, xAxis.data[], title.textConfig strings sourced from customer dataNoEscaped in the default tooltip path; passed verbatim into custom formatters
graphic component elementszrender shape descriptorsNoRendered via zrender (canvas/SVG), not HTML. type: 'image' accepts an arbitrary style.image URL → outbound request from the AppSheet origin
echarts.registerMap(name, geoJson)GeoJSON object or stringNoGeoJSONResource.parseInput uses JSON.parse, falling back to new Function(...)
toolbox.dataView.optionToContentCallback returning HTML/DOMNoNone. String return assigned to viewMain.innerHTML
echarts.registerTheme(name, theme) — used by ChartView.tsx (darkTheme())Theme objectYesFirst-party static theme definition

Trust Boundaries and Auth Assumptions

  • Authentication: None at the library level. ECharts runs entirely in the browser and authenticates nothing. All authentication is performed upstream by AppSheet before chart data is delivered to the client.
  • Authorization: None at the library level. Row-level and table-level access control is enforced by the AppSheet backend and the AppSheet security filter model before rows reach queryRowValues and are passed into chart options.
  • Implicit trust: The library implicitly treats every string in the option object as trusted, renderable markup. This assumption is invalid in AppSheet, where option strings are assembled from two untrusted sources: app-creator-authored view configuration (column display names, titles, aggregations) and end-user-entered record values.
  • Trusted Types: The AppSheet frontend runs under a Trusted Types policy. The fork's patches satisfy that policy without providing sanitization; the browser guardrail that would otherwise block tooltip DOM-XSS is intentionally neutralized for this code path. Any XSS analysis must assume Trusted Types provides no protection for ECharts tooltips.
  • Same-origin blast radius: Charts render in the main AppSheet document, not a sandboxed iframe. Script execution in a chart tooltip runs with full AppSheet origin privileges (session cookies, localStorage, in-page APIs).
  • Boundary crossings: App-creator view definition and end-user record data → AppSheet backend → AppSheet frontend (ChartView.tsx, chart option builders) → ECharts option object → tooltip/label rendering → browser DOM. The critical crossing is the last one, where data leaves the React-escaped world and enters raw HTML string construction.

Sensitive Data Paths

Data TypeSourceDestinationProtection
Customer record values (row/cell data)AppSheet table query (table.queryRowValues)Chart series data, axis categories, pie slice names, tooltip HTMLAccess-controlled server-side; unescaped once inside custom formatter callbacks
Column display names / chart titlesApp creator's saved view definition (ChartConfig)Axis names, series names, title.text, tooltip label prefixesCreator-authored; treated as trusted markup by the library
Customer app names and creator email addresses (PII)AppSheet Admin Console APIsAdmin Console tooltip HTML via the shared tooltipFormatterInterpolated into an HTML string with no escaping
Customer-supplied app logo URLsApp metadatarich[...].backgroundColor.image on axis labelsLoaded as an image resource from the AppSheet origin
Chart bitmap / SVG exportRendered canvas or SVG roottoDataURL, download anchor, or a new browser tabContains whatever customer data is on-screen; written via document.write in the fallback path

Privileged Actions

ActionLocationGuard
HTML injection into the live DOMTooltipHTMLContent.setContent → el.innerHTMLPass-through Trusted Types policy TooltipHTMLFix2 (createHTML: s => s) — no effective guard
Trusted Types policy creation (per render)TooltipHTMLContent.setContent (TooltipHTMLFix1/2/3)None; requires the policy names to be CSP-allow-listed, and duplicates to be permitted
Inline CSS injectionTooltipHTMLContent.show → el.style.cssText (extraCssText)None
Unpoliced HTML injectionTooltipHTMLContent.setContent → arrowEl.innerHTML (DOM-content branch)None
Dynamic code evaluationGeoJSONResource.ts:parseInput → new Function(...)Reachable only when JSON.parse is unavailable
HTML injection (toolbox)DataView.ts → header.innerHTML, viewMain.innerHTML, closeButton.innerHTML, refreshButton.innerHTMLNone; feature is opt-in via toolbox.feature.dataView
New-window document writeSaveAsImage.ts → tab.document.write(html), doc.write(bstr)None; markup built by string concatenation
SVG DOM replacementecharts.ts → (zr.painter as SVGPainter).getSvgRoot().innerHTML = contentNone; connected-charts export path
Outbound network requestgraphic type: 'image' / rich[...].backgroundColor.imageNone; arbitrary URL fetched from the AppSheet origin
Shipping pre-built browser bundlesdist/echarts*.js, dist/echarts*.min.jsCommitted artifacts; not verifiably reproducible from src/ (see below)

Fork-patch coverage across committed bundles — the patches applied to src/ are not uniformly present in dist/:

BundleTooltipHTMLFix1TooltipHTMLFix2TooltipHTMLFix3State
echarts.js / echarts.min.jsyesyesyesMatches src/ (HEAD)
echarts.common.js / .common.min.jsyesyesyesMatches src/ (HEAD)
echarts.esm.js / .esm.min.jsyes (×2)nonoStale at commit 4e53257
echarts.simple.js / .simple.min.jsnononoUnpatched upstream code

package.json sets main: dist/echarts.js (patched) and module: index.js (compiled from patched src/), so the default resolution paths are current. AppSheet imports the library as import('echarts') (full bundle, not tree-shaken), which resolves to main/module. The esm and simple bundles remain divergent and should be regenerated or removed.

Consumer Context (AppSheet)

Recorded here because it determines exploitability of the sinks above.

  • Dependency pin: Nirvana/Content/package.json declares "echarts": "git+https://appsheet-third-party.googlesource.com/echarts" with no #ref and no semver constraint — it tracks the fork‘s default branch. package-lock.json resolves it to #967afa1ae54628568880206a569b42d40189f566 with no integrity hash (npm omits subresource integrity for git dependencies). A push to the fork’s main therefore lands in AppSheet on the next lockfile refresh, with no hash pinning.
  • Primary consumer: Nirvana/Content/components/app/primary/ChartView.tsx renders end-user-facing chart views. Chart configuration is read from the app creator's saved view definition (ChartConfig); row data is pulled via table.queryRowValues and passed into the option builders under src/editor/charts/helpers/create_chart_option/.
  • Function formatters returning HTML built from record data: ChartView.tsx (scatter tooltip, joins value[i] with <br/>), bar_chart_builder.ts (getBarLineAreaTooltips), and scatter_chart_builder.ts all concatenate row values and creator-authored column display names into HTML strings with no escaping.
  • Admin Console: components/admin/console/overview/echarts_options.ts exports a shared tooltipFormatter that interpolates data.name and data.value into a <p>/<span>/<b> HTML string. Its callers pass customer-authored app names (PopularAppsCard.tsx) and creator email addresses (MostAppsOwnedCard.tsx).
  • Rich-text templates fed by data: pie_chart_builder.ts and kpi_helper_functions.ts build {bold|...} / {header|...} rich-text tokens from row values and series names.
  • Configuration not used: No renderMode, appendToBody, appendTo, or graphic configuration appears anywhere in the AppSheet frontend, so tooltips use the default renderMode: 'html' — i.e. the innerHTML path above — and the tooltip DOM stays within the chart container.
  • No sanitization wrapper: No DOMPurify or equivalent escaping is applied to any ECharts formatter output in the AppSheet frontend.

Priority Review Areas

  1. Pass-through Trusted Types policy in TooltipHTMLContent.setContent — the fork's createHTML: (string) => string identity policy launders arbitrary strings into TrustedHTML, silencing the violation while removing the protection; this is the single highest-value finding in the repository and should be replaced with real sanitization.
  2. Function-formatter → innerHTML DOM-XSS chain — AppSheet's tooltip formatters concatenate end-user record values and creator-authored column names into HTML with no escaping, and the library performs none on the callback return path, giving a direct customer-data-to-script-execution route in the AppSheet origin.
  3. extraCssText raw CSS injection — tooltipModel.get('extraCssText') is concatenated straight into el.style.cssText, permitting arbitrary declarations and CSS-based data exfiltration if it is ever fed configuration-derived text.
  4. Unpatched HTML sinks outside the tooltip — DataView.ts, SaveAsImage.ts, the connected-charts SVG export in echarts.ts, and the arrowEl.innerHTML branch received no fork hardening and will either bypass or break under Trusted Types enforcement.
  5. new Function evaluation in GeoJSONResource.parseInput — a dynamic code-execution primitive reachable from registerMap input whenever JSON.parse is unavailable; confirm unreachability or remove the fallback.
  6. dist/ bundle divergence and unpinned dependency resolution — the committed esm and simple bundles do not correspond to src/, the bundles are unverifiable build artifacts shipped to browsers, and AppSheet consumes the fork from a branch reference with no integrity hash, so this is a supply-chain integrity gap in addition to a patch-coverage gap.
  7. Stale upstream baseline — pinned to ECharts 5.4.0 (November 2022) and a zrender nightly pre-release, with no rebase since; any upstream ECharts or zrender security fix released after that date is presumed missing.
  8. Rich-text token injection in canvas labels — unescaped {, }, and | in row values can break out of {style|text} templates in the pie and KPI label formatters, causing label spoofing and rendering-integrity issues (not XSS, as these are canvas-rendered).

Out of Scope

Teams can fill out this section with any known filed vulnerability bugs that they've determined should be out of scope from future Fortify bug filing.