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.