novus/nvd3; per METADATA the fork's base is upstream v1.1.15b, last upgraded 2013-12-18, i.e. a decade-old snapshot that no longer tracks upstream.https://gnocchi-internal.googlesource.com/third_party/nvd3, branch main).package.json declares "version": "0.0.1", and the Grunt uglify banner (GruntFile.js) stamps the artefacts accordingly — the built nv.d3.min.js begins /*! nvd3 - v0.0.1 - 2023-09-13 */. The version string is deliberately detached from upstream numbering, so upstream-version matching by vulnerability scanners will not work on this asset.src/ (the authored sources), and the concatenated/minified build artefacts nv.d3.js and nv.d3.min.js produced from them by GruntFile.js / Makefile. nv.d3.css / .min.css are presentational.[!IMPORTANT] This fork reaches production as a vendored copy, not as an npm/bower dependency. No
package.jsonin AppSheet'sjeeneemonorepo declares a dependency onnvd3— dependency-graph tooling will report this repository as unused. It is not. The build artefact in this repository is byte-identical (verified by MD5,ba459888d567a9f5375c7d90c698e9cf) to the file checked into the AppSheet web frontend atjeenee/Nirvana/Scripts/app/plugins/nv.d3.min.js, which is served to authenticated AppSheet users. This repository is the confirmed build source of live, Internet-facing production JavaScript, and findings here are directly exploitable findings in AppSheet.
Google-authored modification. The single substantive fork change (commits f07d57e “[nwd] Patching nv.d3 trusted types violations” and 189db60 “[nwd] Fixing a bug - DOMPurify was not imported”, kjamali@, Sept 2023) inlines DOMPurify 3.0.5 into the bundle as src/purify.js and calls it from one tooltip sink. Upstream nvd3 has no DOMPurify dependency; src/purify.js is concatenated into nv.d3.js ahead of the library proper. This was done to satisfy AppSheet's Trusted Types enforcement, not as a general XSS hardening pass — see Priority Review Area 1.
Production consumers. The vendored nv.d3.min.js is loaded by four live legacy Razor views in jeenee, all with a CSP nonce:
| View | Line | Surface |
|---|---|---|
V3/MainServer/Views/template/_showTablePartial.cshtml | 91 | App editor — table/column detail |
V3/MainServer/Views/Shared/_logDetailsPartial.cshtml | 76 | Audit-log detail |
V3/MainServer/Views/Shared/_perfDetailsPartial.cshtml | 56 | Performance / expression trace detail |
V3/MainServer/Views/template/commoneditor/appStatsPartial.cshtml | 102 | App usage statistics |
The driver is jeenee/Nirvana/Scripts/sp-theme/manageGraphs.js (loaded by Views/template/AppStats.cshtml:49 and Views/manage/ViewLog.cshtml:194). It builds chart data at manageGraphs.js:72-88 — series key comes from data[i].series and point label from the keys of data[i].data — then calls nv.addGraph(...) at manageGraphs.js:114 (and again at :336 for showTimeGraph). Those series keys and point labels derive from AppSheet app, table, column and view names and from user spreadsheet data, i.e. from attacker-influenceable strings.
nv.d3.min.js is minified JavaScript shipped verbatim to AppSheet editor and admin users.innerHTML, and it bundles a pinned 2023 copy of a security-critical sanitiser (DOMPurify 3.0.5) that receives no upstream updates.footer field all flow from application data into the DOM.A malicious table name, column name or spreadsheet value primarily renders in the creator's own editor, which is self-XSS and low severity on its own. The escalation path is that the same data is rendered on shared and delegated surfaces:
So the realistic threat is stored XSS delivered from a low-privilege app creator to a higher-privilege admin or support operator, not drive-by attack of anonymous users. That is the severity this asset should be triaged at.
src/tooltip.js (highest priority). The file contains two tooltip implementations and the fix was applied to only one:nv.tooltip.show() (the legacy helper) is patched: var safeContent = DOMPurify.sanitize(content, {RETURN_TRUSTED_TYPE: true}); container.innerHTML = safeContent; (src/tooltip.js:340-341, built nv.d3.js:2339-2340).nv.models.tooltip() (the “new way”, per the file's own header comment) is not: its private getTooltipContainer(newContent) does a bare container.node().innerHTML = newContent; (src/tooltip.js:167, built nv.d3.js:2166) with no DOMPurify call anywhere in that code path. Its default contentGenerator builds the markup with D3 .html() calls — .html(headerFormatter(d.value)) (:83), .html(function(p) {return p.key}) (:101), .html(function(p,i) { return valueFormatter(p.value,i) }) (:104) — serialises via table.node().outerHTML, then string-concatenates an unescaped footer: html += "<div class='footer'>" + d.footer + "</div>" (:119-120). Flag every one of these as an XSS sink.nv.models.tooltip is instantiated by nv.interactiveGuideline (src/interactiveLayer.js:11, exposed as layer.tooltip at :157) and invoked as interactiveLayer.tooltip.position(...).data({value, series})() by src/models/lineChart.js:276, src/models/stackedAreaChart.js:401 and src/models/cumulativeLineChart.js:498 — but only when the consumer calls chart.useInteractiveGuideline(true). Scanners should treat this as a live, reachable sink in the shipped bundle and as a public API (nv.models.tooltip is exported on window.nv.models).src/purify.js is a frozen copy of DOMPurify 3.0.5 (DOMPurify.version = '3.0.5', src/purify.js:319), released 2023. It is inlined into nv.d3.js/nv.d3.min.js, so it will never be updated by npm audit, Dependabot, or any lockfile-based tooling, and it is invisible to SCA that keys off package.json. Check 3.0.5 against subsequently disclosed DOMPurify bypasses and mutation-XSS issues, and flag the absence of any update mechanism as a finding in its own right.nv.utils.pjax — network fetch plus history rewrite. src/utils.js:87-105 binds click handlers that call history.pushState(this.href, …) and then d3.html(href, …), parsing the response and splicing a selected node into the live DOM via target.parentNode.replaceChild(...). There is no origin or scheme check on href. This is a general-purpose fetch-and-inject primitive with no relationship to charting.clip-path URL references by string concatenation from a chart id — e.g. src/models/historicalBar.js:95,98,102 ('nv-chart-clip-path-' + id), src/models/line.js:99,101 ('url(#nv-edge-clip-' + scatter.id() + ')'), and the equivalents in multiBar.js, multiBarTimeSeries.js, ohlcBar.js, scatter.js, lineWithFisheye.js. Those IDs are also fed back into wrap.select('#…') selectors. Verify that no caller can set a chart id to an attacker-controlled string and thereby break out of the ID/selector context.nv.d3.js and nv.d3.min.js are hand-committed, not produced by CI. Confirm they are faithful builds of src/ at every commit; the minified artefact is what actually ships.| Entry Point | Type | Trusted? | Validation |
|---|---|---|---|
Chart datum series[].key | JS data → DOM | No — derives from app/table/column/view names (manageGraphs.js:76) | nv.models.tooltip default contentGenerator: .html(function(p){return p.key}) — none. Legacy nv.tooltip.show path: DOMPurify |
Chart datum point label / value | JS data → DOM | No — derives from user spreadsheet data (manageGraphs.js:81-82) | Passed through valueFormatter then .html(...) — no escaping in nv.models.tooltip |
d.value (tooltip header) | JS data → DOM | No | .html(headerFormatter(d.value)) — headerFormatter defaults to identity, no escaping |
d.footer | JS data → DOM | No | String-concatenated into "<div class='footer'>" + d.footer + "</div>" — no escaping |
chart.tooltipContent(fn) / contentGenerator(fn) | JS API | Caller-supplied | Caller-defined HTML string; the default in e.g. src/models/multiBarChart.js:28-31 concatenates key, x, y into <h3>/<p> markup |
chart.xAxis.axisLabel(text) | JS API → SVG | No — options.xAxisName (manageGraphs.js:106,108) | Rendered via D3 .text() on an SVG <text> node in src/models/axis.js (not .html()), so this is text-context and comparatively safe |
nv.utils.pjax(links, content) link href | DOM attribute → HTTP | No | None. No scheme or origin allowlist before d3.html(href, …) |
Chart id | JS API → SVG ids and selectors | Caller-supplied | Concatenated into clipPath id attributes and #… selectors without escaping |
manageGraphs.js maps it into series[].key and point label.nv.models.tooltip is exactly at that boundary.nonce="@CspNonce", indicating a nonce-based CSP. A correct nonce CSP blocks injected <script> elements. This limits, but does not eliminate, impact: injected markup, event-handler attributes rejected by CSP still leave overlay/UI-redress and data-exfiltration-via-markup avenues open, and the protection evaporates if script-src carries a permissive fallback.container.node().innerHTML = newContent in getTooltipContainer would be blocked by TT enforcement rather than sanitised — meaning under full TT the unsanitised path fails closed (tooltip breaks) rather than executing. Confirm whether TT is enforced (not report-only) on each of the four consuming routes; if it is report-only, the sink is live.nv.models.multiBarChart (manageGraphs.js:46, V3/MainServer/Views/template/commoneditor/_perfDisplayPartial.cshtml:148), whose tooltip routes through the DOMPurify-sanitised nv.tooltip.show (src/models/multiBarChart.js:72). AppSheet does not currently call useInteractiveGuideline(true), so the unsanitised nv.models.tooltip path is not exercised by today's call sites. It remains shipped, exported, and one line of caller code away from being live — which is why it is still ranked first below.| Data Type | Source | Destination | Protection |
|---|---|---|---|
| App / table / column / view names | Customer-authored app schema | series[].key → tooltip DOM | DOMPurify on the nv.tooltip.show path; none on the nv.models.tooltip path |
| Spreadsheet cell values | Customer / end-user data source | Point label / value → tooltip and axis DOM | Axis labels use .text() (safe); tooltip values use .html() (unprotected on the nv.models.tooltip path) |
| End-user activity and identifiers | AppSheet audit log | _logDetailsPartial.cshtml charts | Rendered in an admin/support session; same tooltip sinks |
| App usage statistics | AppSheet telemetry | appStatsPartial.cshtml / AppStats.cshtml charts | Same tooltip sinks; cross-tenant viewers possible |
| Expression / performance traces | AppSheet app compiler & runtime | _perfDetailsPartial.cshtml charts | Same tooltip sinks; traces may embed user formulas and column names |
| Action | Location | Guard |
|---|---|---|
Assign untrusted string to innerHTML | src/tooltip.js : nv.models.tooltip → getTooltipContainer | None |
Assign string to innerHTML after sanitisation | src/tooltip.js : nv.tooltip.show | DOMPurify.sanitize(content, {RETURN_TRUSTED_TYPE: true}) |
| Render untrusted values as HTML via D3 | src/tooltip.js : nv.models.tooltip default contentGenerator (.html() × 3, plus footer concatenation) | None |
| Outbound HTTP fetch of a DOM-supplied URL and DOM replacement | src/utils.js : nv.utils.pjax → load (d3.html, replaceChild) | None. No scheme/origin allowlist |
| Browser history mutation | src/utils.js : nv.utils.pjax (history.pushState) | None |
| Bundled HTML sanitiser (security-critical dependency) | src/purify.js (DOMPurify 3.0.5), inlined into nv.d3.js / nv.d3.min.js | Pinned; no update path, invisible to SCA |
SVG id / clip-path / selector construction from a chart id | src/models/{historicalBar,line,multiBar,multiBarTimeSeries,ohlcBar,scatter,lineWithFisheye,discreteBarChart}.js | String concatenation, no escaping |
nv.models.tooltip was left out of the DOMPurify fix (src/tooltip.js → getTooltipContainer and the default contentGenerator). The 2023 patch sanitised the legacy nv.tooltip.show helper and stopped there, even though the file‘s own header comment calls nv.models.tooltip “the updated, new way to render tooltips”. The result is a shipped, exported tooltip renderer that takes series keys, point values, header values and a footer straight into .html() and innerHTML with no escaping and no sanitiser. It is invoked in the bundle by lineChart/stackedAreaChart/cumulativeLineChart under useInteractiveGuideline(true). Accuracy note: AppSheet’s current call sites use only multiBarChart, which routes through the sanitised helper, so this is a latent rather than presently-exercised sink — but it is one useInteractiveGuideline(true) away, and the same file already proves the project knows this data is untrusted. Fix by routing getTooltipContainer through DOMPurify.sanitize(..., {RETURN_TRUSTED_TYPE: true}) and replacing the .html() calls with .text().src/purify.js). A security control frozen at a 2023 release and concatenated into a build artefact is invisible to every dependency scanner AppSheet runs. Establish ownership and an update cadence, check 3.0.5 against subsequently published bypasses, and prefer consuming DOMPurify as a real, version-pinned dependency over an inlined copy.nv.utils.pjax (src/utils.js). A fetch-and-inject plus history.pushState primitive with no origin validation, shipped inside a charting library. Determine reachability: no AppSheet code references nv.utils.pjax, and it serves no charting purpose. Recommendation: delete it from the fork — it is pure attack surface with zero product value.script-src has no permissive fallback and determine whether require-trusted-types-for 'script' is enforcing or report-only. These two controls are what currently bound the impact of every sink above.id propagation into clipPath id attributes, url(#…) references, and d3.select('#'+id) selectors across src/models/ for injection or selector-confusion.bower.json pins d3 ~3.3.5). Neither receives security maintenance. Evaluate migrating these four legacy views off nvd3 entirely as the durable remediation.jeenee/Nirvana/Scripts/app/plugins/nv.d3.indentedTree.js is NOT produced by this fork. Its header comment pins it to upstream https://github.com/novus/nvd3/blob/v1.2.1/src/models/indentedTree.js — a different upstream version from this repository's v1.1.15b base — and it is a separately vendored, standalone file in the jeenee repository. It is loaded alongside nv.d3.min.js by _showTablePartial.cshtml:92, _logDetailsPartial.cshtml:77 and _perfDetailsPartial.cshtml:57, and instantiated as nv.models.indentedTree() at _showTablePartial.cshtml:207 and _perfDetailsPartial.cshtml:352. This repository does contain a src/models/indentedTree.js, but it is not the file that ships. nv.d3.indentedTree.js must be reviewed on the jeenee side, not here.deprecated/, examples/, test/, lib/ — sample pages, fixtures and vendored demo dependencies with no production code path.src/models/backup/ — superseded model sources retained in-tree; not concatenated into the build.GruntFile.js, Makefile, build.bat, .jshintrc and the Grunt/Babel devDependencies — build-time only, not executed in CI for this repository, and producing no automatically served artefact.nv.d3.css / nv.d3.min.css — stylesheets with no scripting surface.d3 (v3), supplied separately by AppSheet; it is not vendored in this repository.manageGraphs.js, and the CSP/Trusted Types middleware. All live in the jeenee repository and must be reviewed there; they are referenced here only as context and as mitigating controls to be verified.