d3/d3; per METADATA the fork's base is upstream v3.4.2, released February 2014. D3 v3 is end-of-life; upstream is now at v7 and no security fixes are backported to the v3 line.https://gnocchi-internal.googlesource.com/third_party/d3, branch main).6776099 “Initial pristine commit of AppSheet OSS D3 dependency” (parentless import of upstream tag v3.4.2, 4637ee5a49ec9f2edea217869a0b4f43b3617912) and b081036 “Remove the jsdom dependency and code we do not use”.METADATA license_type: NOTICE).d3.min.js (146.8 KB — the artefact that ships), d3.js (326.2 KB, the readable build), and src/ (270 authored source files from which both are generated).[!IMPORTANT] This fork is the confirmed build source of live, Internet-facing production JavaScript.
d3.min.jsin this repository is byte-identical (MD533a4640a0fc7e2a0ab47ec00b506f4c7, verified) to the file checked into the AppSheet web frontend atjeenee/Nirvana/Scripts/app/plugins/d3.min.js, which is served to authenticated AppSheet users. Findings here are directly exploitable findings in AppSheet.
Version identity is intact — unlike the sibling nvd3 fork. package.json keeps "version": "3.4.2", and bin/start stamps that value into src/start.js as var d3 = {version: "3.4.2"}, so the runtime global reports d3.version === "3.4.2". Version-keyed SCA and advisory matching will work against this asset. (The nvd3 fork deliberately detaches its version string; that caveat does not apply here.)
Migration in flight. AppSheet is moving from the vendored copy to consuming this repository directly as an npm dependency (git+https://appsheet-third-party.googlesource.com/d3#<sha>), loaded through a webpack shim (scripts/_shared/d3Init.ts) that exposes window.d3. Before and after that migration this repository is the source of truth for the shipped bytes; only the delivery path changes.
d3.min.js is loaded by four live legacy Razor views, all with a CSP nonce:
| View | Line | Surface |
|---|---|---|
V3/MainServer/Views/template/commoneditor/appStatsPartial.cshtml | 101 | App usage statistics |
V3/MainServer/Views/template/_showTablePartial.cshtml | 92 | App editor — table/column detail |
V3/MainServer/Views/Shared/_perfDetailsPartial.cshtml | 57 | Performance / expression trace detail |
V3/MainServer/Views/Shared/_logDetailsPartial.cshtml | 77 | Audit-log detail |
These are the same four views that load nvd3. D3 is present primarily as nvd3's prerequisite: nvd3 requires a global d3 (v3) and its threat model explicitly scopes D3 out — “D3.js itself. The library requires a global d3 (v3), supplied separately by AppSheet; it is not vendored in this repository.” This document closes that gap. The two assets must be read together.
Direct D3 API use by AppSheet code is small and well-bounded:
| Call | Location |
|---|---|
d3.select('#' + $graph.attr('id')) | Nirvana/Scripts/sp-theme/manageGraphs.js:117, :343 |
d3.select($selector[0]) | _showTablePartial.cshtml:235, _perfDetailsPartial.cshtml:387 |
d3.select('#LogVirtualColumnPerformanceGraph .graphDisplay') | _perfDisplayPartial.cshtml:172 |
d3.format(',.0f') | manageGraphs.js:139, :304 |
d3.time.days(...) / d3.time.scale | manageGraphs.js:182, :324 |
Nothing in AppSheet calls d3.csv, d3.tsv, d3.json, d3.html, d3.xhr, d3.text, d3.xml, or selection.html() directly. The highest-severity primitives catalogued below are therefore shipped and exported but not presently exercised by AppSheet's own code — latent rather than live. They remain one line of caller code away, and .html() is reached indirectly through nvd3 (see Prompt 2).
Commit b081036. Deletions and metadata only — no behavioural patches.
index.js, the jsdom-based server-side shim. Because upstream declared jsdom as a production dependency, npm promoted its entire subtree into AppSheet's production closure: 540 packages with it, 498 without — 42 packages attributable solely to the d3 → jsdom edge, along with the five manual licence-review issues they generated. Removing it is the stated reason the fork exists, per METADATA.test/ (209 files, the only other jsdom consumer), bin/bower, bin/component, bower.json, component.json, composer.json, .travis.yml, CONTRIBUTING.md.package.json: "main" index.js → d3.js; removed dependencies.jsdom; removed vows / seedrandom devDependencies and scripts.test; replaced the legacy "licenses": [{"type": "BSD", …}] array with the SPDX "license": "BSD-3-Clause".Makefile: dropped bower.json / component.json from GENERATED_FILES, dropped the test: target and the now-dead %.json: bin/% pattern rule.[!NOTE]
d3.jsandd3.min.jswere not touched. Their MD5s are unchanged from upstream v3.4.2. This is the single most useful security property of this fork: the shipped artefact is stock upstream, so published upstream analysis of D3 3.4.2 applies verbatim, and any diff against upstream is a finding.
d3.min.js is minified JavaScript shipped verbatim to AppSheet editor and admin users.new Function code-generation sink, an innerHTML sink, a createContextualFragment sink, and a general-purpose XHR primitive with no origin controls.d3.selection.html(), not through AppSheet's own D3 calls.Identical to nvd3‘s, because the hosting pages are identical: a malicious table name, column name or spreadsheet value primarily renders in the creator’s own editor (self-XSS, low severity alone), but the same data is rendered for team/organisation admins viewing apps they did not author and for AppSheet support and operations staff investigating tickets. The realistic threat is stored XSS delivered from a low-privilege app creator to a higher-privilege admin or support operator.
new Function code generation in src/dsv/dsv.js:35-37 (highest priority). dsv.parse compiles a row-accessor function at runtime:var a = new Function("d", "return {" + row.map(function(name, i) { return JSON.stringify(name) + ": d[" + i + "]"; }).join(",") + "}");The generated function body embeds the CSV/TSV header row, i.e. attacker influenceable text, and
JSON.stringify(name) is the only thing separating that text from generated source. Flag on two independent grounds:new Function requires unsafe-eval. This is the same class of defect AppSheet already patched out of jquery-ajax-unobtrusive. Under an enforcing policy this path fails closed (DSV parsing throws) rather than executing.JSON.stringify escapes quotes and backslashes, but does not escape U+2028 / U+2029; before ES2019 those were line terminators inside JS string literals and would break out of the generated literal. Modern engines (ES2019+) permit them in string literals, so there is no breakout on a current browser matrix — confirm the supported matrix rather than assuming it.new Function occurrence exists in the shipped d3.js (verified by count). No AppSheet code calls d3.csv / d3.tsv / dsv.parse.selection.html() → innerHTML (src/selection/html.js:6-9). Three innerHTML writes (function-valued, null-clearing and constant), plus one read in the getter branch. No escaping or sanitisation — by design, it is the raw-HTML API. This is the sink that actually matters in AppSheet, because nvd3 reaches it: nvd3‘s nv.models.tooltip default contentGenerator makes three .html() calls on untrusted series keys and values, and that path is not covered by nvd3’s DOMPurify patch. Cross-reference Priority Review Area 1 of the nvd3 threat model; the sink lives here, the unsanitised caller lives there.d3.html() — fetch and parse HTML (src/xhr/html.js).function d3_html(request) { var range = d3_document.createRange(); range.selectNode(d3_document.body); return range.createContextualFragment(request.responseText); }
createContextualFragment is a named Trusted Types sink and will construct live <script> elements from the response. Combined with prompt 4's absent origin controls this is a fetch-and-inject primitive — the D3 analogue of nv.utils.pjax, which the nvd3 model recommends deleting outright.d3.xhr — unrestricted outbound request (src/xhr/xhr.js). request.open(method, url, true) at :94 with no scheme or origin allowlist, caller-controlled setRequestHeader at :96, caller-controlled responseType at :98, and an XDomainRequest fallback at :24-26 for legacy IE. Runs with the page's ambient session credentials. Underlies d3.json, d3.text, d3.xml, d3.html and the DSV loaders.src/locale/time-format.js, src/dsv/dsv.js, src/selection/on.js, src/selection/classed.js. time-format.js is the one that parses caller-supplied format and locale strings.d3.select('#' + $graph.attr('id')) (manageGraphs.js:117, :343) builds a CSS selector by string concatenation from a live DOM attribute. Determine whether the graph element id can be influenced by app or table names.d3.js and d3.min.js are hand-committed, not produced by CI. The build chain (Makefile → smash ~0.0.12 + uglify-js 2.4.0) is declared but those devDependencies are not installed or lockfile-pinned, so the build is not readily reproducible. Current best evidence of integrity is MD5 parity with the upstream v3.4.2 release — re-verify that at every commit, because the minified artefact is what ships.d3@3.4.2 and treat “no fix available upstream” as the expected result.d3.map / d3.set: mitigated. src/arrays/map.js prefixes every key with "\0" (var d3_map_prefix = "\0", // prevent collision with built-ins) in get, set, has, remove and forEach, so __proto__, constructor and friends cannot reach the prototype chain.d3-color ReDoS (CVE-2020-7746 class): not applicable. That advisory targets the standalone d3-color package of the v4+ modular era. In 3.4.2 colour parsing lives in src/color/*.js and is a character-scan parser — no regular expressions at all in that directory (verified).b081036; there is no server-side execution path in this fork.lib/ never ships. lib/ contains vendored third-party material (colorbrewer/colorbrewer.js, science/science*.js, plus LICENSE-only directories for geographiclib, penner, polymaps, protovis, rhill-voronoi). It is excluded by .npmignore and verified absent from npm pack output, so it reaches neither node_modules nor the browser. It is not concatenated into d3.js by the Makefile.| Entry Point | Type | Trusted? | Validation |
|---|---|---|---|
selection.html(value) | JS API → innerHTML | No — reached by nvd3 with customer-authored series keys and values | None by design. Raw-HTML API |
DSV header row → dsv.parse | Text → generated JS source | No — arbitrary CSV/TSV header text | JSON.stringify quoting only; not a security boundary. No AppSheet caller |
| DSV body rows | Text → JS objects | No | Character-scan tokeniser; values become plain object properties |
d3.xhr(url, …) / d3.json / d3.text / d3.xml / d3.html | JS API → HTTP | Caller-supplied | None. No scheme or origin allowlist before request.open |
d3.html response body | HTTP → DOM fragment | No — remote content | createContextualFragment, a Trusted Types sink. No sanitisation |
Request headers via xhr.header(name, value) | JS API → HTTP | Caller-supplied | None |
d3.select(selector) / selectAll | String → CSS selector | Caller-supplied; AppSheet concatenates a DOM id at manageGraphs.js:117, :343 | None. Passed to querySelector |
selection.attr(name, value) | JS API → DOM attribute | Caller-supplied | d3.ns.qualify resolves namespaces (incl. xlink); no URL scheme filtering, so xlink:href / href accept javascript: |
selection.style / .classed / .property | JS API → DOM | Caller-supplied | None |
d3.time.format(specifier) / locale strings | String → RegExp | Caller-supplied | Regex construction — see ReDoS prompt |
.html() is supposed to write raw HTML, d3.xhr is supposed to fetch any URL. The security boundary is the caller, and in AppSheet the relevant caller is nvd3, not AppSheet code.manageGraphs.js maps it into nvd3 series[].key and point label.contentGenerator passes it to d3.selection.html(), which assigns it to innerHTML — the sink in this repository. Step 4 is the trust boundary.nonce="@CspNonce", indicating a nonce-based CSP. A correct nonce CSP blocks injected <script> elements, and — relevant to prompt 1 — a policy without unsafe-eval blocks new Function outright. This limits, but does not eliminate, impact: overlay/UI-redress and markup-based data exfiltration remain, and the protection evaporates if script-src carries a permissive fallback.innerHTML, createContextualFragment and new Function are all TT-enforced sinks. Under enforcement each fails closed rather than executing. Confirm whether require-trusted-types-for 'script' is enforcing or report-only on each of the four consuming routes; if report-only, all three sinks are live.d3.select, d3.format and d3.time. The DSV codegen, the XHR family and d3.html have no caller in the product today.d3.js / d3.min.js, there is no Google-authored code in the shipped bytes to review — a materially better position than the nvd3 fork, which inlines a pinned DOMPurify.| Data Type | Source | Destination | Protection |
|---|---|---|---|
| App / table / column / view names | Customer-authored app schema | nvd3 series[].key → d3.selection.html() → innerHTML | None in D3. DOMPurify on nvd3's legacy tooltip path only |
| Spreadsheet cell values | Customer / end-user data source | nvd3 point label / value → .html() / SVG .text() | .text() paths are safe; .html() paths unprotected |
| End-user activity and identifiers | AppSheet audit log | _logDetailsPartial.cshtml charts | Rendered in an admin/support session; same sinks |
| App usage statistics | AppSheet telemetry | appStatsPartial.cshtml charts | Same sinks; cross-tenant viewers possible |
| Expression / performance traces | AppSheet app compiler & runtime | _perfDetailsPartial.cshtml charts | Same sinks; traces may embed user formulas and column names |
| Session cookies / ambient credentials | Browser | Any d3.xhr destination | None. No origin allowlist; not currently invoked by AppSheet |
| Action | Location | Guard |
|---|---|---|
| Compile a function from a string at runtime | src/dsv/dsv.js:35 (new Function) | None. JSON.stringify quoting of header names only |
Assign an arbitrary string to innerHTML | src/selection/html.js:6-9 | None (raw-HTML API by design) |
| Parse remote HTML into a live DOM fragment | src/xhr/html.js (createContextualFragment) | None |
| Outbound HTTP request to a caller-supplied URL | src/xhr/xhr.js:94 (request.open) | None. No scheme/origin allowlist |
| Set arbitrary request headers | src/xhr/xhr.js:96 (setRequestHeader) | None |
| Cross-origin request via legacy fallback | src/xhr/xhr.js:24-26 (XDomainRequest) | Scheme regex /^(http(s)?:)?\/\// only |
Set arbitrary DOM attributes, incl. xlink:href / href | src/selection/attr.js via d3.ns.qualify | None. No URL scheme filtering |
| Construct RegExp from caller-supplied format/locale strings | src/locale/time-format.js | None |
| Resolve a CSS selector built by string concatenation | manageGraphs.js:117, :343 (AppSheet side) | None |
new Function code generation in src/dsv/dsv.js — the one Google-relevant design defect in the shipped bundle. It is unreachable from AppSheet today and blocked by an enforcing CSP, but it is a runtime-codegen sink whose body is built from untrusted header text, and it is precisely the pattern AppSheet has already paid down elsewhere (jquery-ajax-unobtrusive). Recommendation: since AppSheet never calls d3.csv / d3.tsv, consider deleting src/dsv/ from the fork and rebuilding — the same “delete unused attack surface” reasoning the nvd3 model applies to nv.utils.pjax, and consistent with this fork's existing deletion-only posture. Note this would change d3.min.js, forfeiting the upstream-parity property, so weigh it deliberately.selection.html() reached by nvd3's unsanitised tooltip path. The sink is in this repository; the vulnerable caller is in the nvd3 fork. Neither document is actionable alone. Triage these two assets as a single unit, and fix at the nvd3 end (getTooltipContainer and the default contentGenerator), not here — D3's .html() is behaving as specified.d3.html + d3.xhr as a fetch-and-inject primitive. No origin controls, a Trusted Types sink on the parse side, ambient credentials on the fetch side, and no AppSheet caller. Same disposition question as src/dsv/: delete, or accept-and-document.unsafe-eval absence and Trusted Types enforcement mode on the four consuming routes. These two controls are what currently bound the impact of items 1–3. If TT is report-only, all three sinks are live.d3.js / d3.min.js can be regenerated from src/ — pin smash and uglify-js, or record that the artefacts are accepted as upstream-verbatim and enforce that with an MD5 check in review.manageGraphs.js:117 / :343. AppSheet-side, but it is the only place AppSheet feeds a concatenated string into a D3 API. Confirm the graph id cannot carry attacker-controlled characters.https://gnocchi-internal.googlesource.com/third_party/nvd3 with its own THREAT_MODEL.md. Its tooltip sanitisation gap is the principal way untrusted data reaches D3‘s innerHTML sink, so the two documents are complementary and should be triaged together — but nvd3’s own code must be reviewed there.jeenee/Nirvana/Scripts/app/plugins/nv.d3.indentedTree.js — a separately vendored file from a different nvd3 version, not produced by either fork. Review on the jeenee side.lib/ — vendored demo dependencies (colorbrewer, science.js) and LICENSE-only directories. Excluded by .npmignore, verified absent from npm pack, not referenced by the Makefile, never served.Makefile, bin/start, bin/uglify, and the smash / uglify-js devDependencies — build-time only. No CI executes them for this repository and they produce no automatically served artefact. Retained so the build remains auditable; see Priority Review Area 6.src/start.js / src/end.js — the IIFE wrapper and version stamp generated from package.json.index.js (jsdom shim), test/, bower.json, component.json, composer.json, .travis.yml. Absent from main; review only if resurrected.manageGraphs.js, the webpack d3Init.ts shim, and the CSP / Trusted Types middleware. All live in the jeenee repository and must be reviewed there; referenced here only as context and as mitigating controls to be verified.