Add Fortify scanner THREAT_MODEL.md to third_party/echarts repo Bug: b/548456054 Change-Id: I40cf5459b96539211ede22328f69768fead3beb2 Reviewed-on: https://gnocchi-internal-review.git.corp.google.com/c/third_party/echarts/+/316668 Reviewed-by: Dmitry Riegle <riegle@google.com>
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md new file mode 100644 index 0000000..cfe684e --- /dev/null +++ b/THREAT_MODEL.md
@@ -0,0 +1,296 @@ +# 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 Point | Type | Trusted? | Validation | +|---|---|---|---| +| `echartsInstance.setOption(option)` — the primary API; receives the full chart config | Public API / config object | No | No 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.ts` | Callback returning HTML | No | **None.** Return value flows to `el.innerHTML` via the pass-through TT policy | +| `tooltip.formatter` (string template) | HTML template with `{a}/{b}/{c}` | No | Placeholders escaped by `formatTpl(..., encode=true)` → `encodeHTML`; the surrounding template is raw HTML | +| Default tooltip markup (no `formatter`) | Internal markup builder | No | `tooltipMarkup.ts` applies `encodeHTML` to series names and values — safe baseline | +| `tooltip.extraCssText` | Raw CSS string | No | **None.** Concatenated directly into `el.style.cssText` | +| `series.label.formatter` / `axisLabel.formatter` / legend formatter | String or callback | No | Rendered as canvas/SVG text, not HTML; rich-text `{style|text}` tokens are parsed without escaping data values | +| `series[].name`, `series.data[].name`, `xAxis.data[]`, `title.text` | Config strings sourced from customer data | No | Escaped in the default tooltip path; passed verbatim into custom formatters | +| `graphic` component elements | zrender shape descriptors | No | Rendered 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 string | No | `GeoJSONResource.parseInput` uses `JSON.parse`, falling back to `new Function(...)` | +| `toolbox.dataView.optionToContent` | Callback returning HTML/DOM | No | **None.** String return assigned to `viewMain.innerHTML` | +| `echarts.registerTheme(name, theme)` — used by `ChartView.tsx` (`darkTheme()`) | Theme object | Yes | First-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 Type | Source | Destination | Protection | +|---|---|---|---| +| Customer record values (row/cell data) | AppSheet table query (`table.queryRowValues`) | Chart series data, axis categories, pie slice names, tooltip HTML | Access-controlled server-side; **unescaped** once inside custom `formatter` callbacks | +| Column display names / chart titles | App creator's saved view definition (`ChartConfig`) | Axis names, series names, `title.text`, tooltip label prefixes | Creator-authored; treated as trusted markup by the library | +| Customer app names and creator email addresses (PII) | AppSheet Admin Console APIs | Admin Console tooltip HTML via the shared `tooltipFormatter` | Interpolated into an HTML string with no escaping | +| Customer-supplied app logo URLs | App metadata | `rich[...].backgroundColor.image` on axis labels | Loaded as an image resource from the AppSheet origin | +| Chart bitmap / SVG export | Rendered canvas or SVG root | `toDataURL`, download anchor, or a new browser tab | Contains whatever customer data is on-screen; written via `document.write` in the fallback path | + +## Privileged Actions + +| Action | Location | Guard | +|---|---|---| +| HTML injection into the live DOM | `TooltipHTMLContent.setContent` → `el.innerHTML` | Pass-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 injection | `TooltipHTMLContent.show` → `el.style.cssText` (`extraCssText`) | None | +| Unpoliced HTML injection | `TooltipHTMLContent.setContent` → `arrowEl.innerHTML` (DOM-content branch) | None | +| Dynamic code evaluation | `GeoJSONResource.ts:parseInput` → `new Function(...)` | Reachable only when `JSON.parse` is unavailable | +| HTML injection (toolbox) | `DataView.ts` → `header.innerHTML`, `viewMain.innerHTML`, `closeButton.innerHTML`, `refreshButton.innerHTML` | None; feature is opt-in via `toolbox.feature.dataView` | +| New-window document write | `SaveAsImage.ts` → `tab.document.write(html)`, `doc.write(bstr)` | None; markup built by string concatenation | +| SVG DOM replacement | `echarts.ts` → `(zr.painter as SVGPainter).getSvgRoot().innerHTML = content` | None; connected-charts export path | +| Outbound network request | `graphic` `type: 'image'` / `rich[...].backgroundColor.image` | None; arbitrary URL fetched from the AppSheet origin | +| Shipping pre-built browser bundles | `dist/echarts*.js`, `dist/echarts*.min.js` | Committed 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/`: + +| Bundle | `TooltipHTMLFix1` | `TooltipHTMLFix2` | `TooltipHTMLFix3` | State | +|---|---|---|---|---| +| `echarts.js` / `echarts.min.js` | yes | yes | yes | Matches `src/` (HEAD) | +| `echarts.common.js` / `.common.min.js` | yes | yes | yes | Matches `src/` (HEAD) | +| `echarts.esm.js` / `.esm.min.js` | yes (×2) | **no** | **no** | Stale at commit `4e53257` | +| `echarts.simple.js` / `.simple.min.js` | **no** | **no** | **no** | Unpatched 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.