Security Threat Model: jQuery (AppSheet Trusted-Types Fork)

Asset Definition & Scope

  • Component: jQuery — client-side JavaScript DOM manipulation, traversal, event, effects, and AJAX library.
  • Repository: Git-on-Borg (https://gnocchi-internal.googlesource.com/third_party/jquery, branch main). Mirrored on the legacy host https://appsheet-third-party.googlesource.com/jquery, which is the URL the AppSheet frontend actually resolves at install time.
  • Upstream baseline: jQuery 3.6.2 (upstream release 2022-12-13), including the bundled Sizzle 2.3.8 selector engine (external/sizzle/dist/sizzle.js). Declared in METADATA as version: "3.6.2", last_upgrade_date: 2023-03-01.
  • Fork delta: This is a Trusted-Types-patched build of jQuery. Relative to the pristine upstream 3.6.2 tarball, only four source files differ, all of them to remove innerHTML writes performed during browser feature detection:
    • src/core/support.js — under Trusted Types, short-circuits the Safari 8 support.createHTMLDocument probe instead of writing body.innerHTML = "<form></form><form></form>".
    • src/manipulation/support.js — under Trusted Types, hardcodes support.noCloneChecked = true and support.option = true instead of probing via div.innerHTML = "<textarea>x</textarea>" / "<option></option>".
    • external/sizzle/dist/sizzle.js — three sites: two assert() probes rebuilt with document.createElement / setAttribute / appendChild instead of innerHTML, and the legacy IE<8 / IE<9 addHandle( "type|href|height|width" ) and addHandle( "value" ) attribute-interpolation probes skipped entirely when window.trustedTypes.createPolicy exists.
    • src/core.js — version reverted to the "@VERSION" build placeholder.
    • Additionally the repo commits a locally produced dist/jquery.js, dist/jquery.min.js, and dist/jquery.min.map (banner v3.6.2-pre, Date: 2023-03-15T20:55Z), plus Google METADATA and LICENSE files.
  • Explicitly NOT changed: no CVE backports, no security hardening, and no modification to any runtime HTML sink. jQuery.fn.html, buildFragment, jQuery.parseHTML, jQuery.htmlPrefilter, jQuery.globalEval, DOMEval, and the Sizzle selector parser are byte-identical to upstream 3.6.2.
  • Scope: The full library source (src/), the vendored selector engine (external/sizzle/), and the committed prebuilt bundles (dist/) that are the artifacts actually consumed by AppSheet.

Consumption by AppSheet

  • Pinned by exact commit in jeenee/Nirvana/Content/package.json: "jquery": "git+https://appsheet-third-party.googlesource.com/jquery#1d60e5afef1b0287e855686a4d524d8cf3aa61ee" (with @types/jquery 3.5.14).
  • A single ES import in Nirvana/Content/scripts/_shared/external/jqueryInit.js (import $ from 'jquery'; window.$ = window.jQuery = $;) publishes jQuery as a global; roughly 75 first-party .ts/.tsx files then consume $ / jQuery without importing it, concentrated in scripts/legacy/** (Form, List, Gallery, Deck, Calendar, Dashboard, GoogleMap, Graph, Search, FastTable, SlideShow controllers), scripts/_shared/lib/**, and scripts/_shared/models/LegacyViewManager.tsx.
  • webpack.config.js resolves the package main (dist/jquery.js, unminified) into every SPA entry point (app-main, editor-main, site-main, my-apps-main, templates-main, app-gallery-main) and Terser-minifies it into the shipped build/[name].js; jQuery is explicitly excluded from babel-loader.
  • Server-rendered Razor views additionally ship a pre-minified copy via <script nonce="@CspNonce" src="/Scripts/jquery-3.6.2.min.js"> (V3/MainServer/Views/Shared/_LegacyContentLayout.cshtml and friends).
  • Bundled jQuery plugins that extend the same $ object: _shared/external/jqueryFileTree, jquery.exif, jquery.canvasresize.

Prioritization Signals

  • 1P OSS: No (third-party open-source library maintained as an internal customized fork for Google AppSheet; upstream is https://github.com/jquery/jquery, MIT).
  • 1P Proprietary Shipped Software: Yes. Although the code itself is third-party OSS, this fork‘s dist/jquery.js is compiled and minified into the proprietary AppSheet web bundles that are served to customer browsers, and a minified copy is served directly as /Scripts/jquery-3.6.2.min.js. It therefore executes as shipped, minified first-party JavaScript inside every AppSheet end-user and app-editor session, in the same origin and with the same privileges as AppSheet’s own code. A defect here is a defect in shipped AppSheet client software, not merely in a build-time dependency.
  • High-Risk Code Surface: Yes. jQuery is fundamentally a collection of DOM-injection sinks: string-to-DOM HTML parsing (buildFragment → tmp.innerHTML, jQuery.htmlPrefilter, jQuery.parseHTML, jQuery.fn.init HTML-string coercion), dynamic script execution (jQuery.globalEval → DOMEval script-element injection, the text script AJAX converter, JSONP callback injection in src/ajax/jsonp.js), a hand-written regex-based CSS selector parser (Sizzle), attribute/property writers, and a deep-merge utility (jQuery.extend). The fork additionally disables several feature-detection code paths, which changes runtime behavior.
  • Perimeter Exposure: Yes. AppSheet is a 1P Google Cloud service exposed to the public Internet; this library is downloaded and executed by every unauthenticated and authenticated visitor of AppSheet app pages, the app editor, the site/marketing pages, and the app gallery.
  • Data Sensitivity: High. jQuery-driven legacy view controllers render customer business records, PII, file/attachment metadata, account and billing surfaces, and app-definition metadata directly into the DOM. Because it runs in the AppSheet origin, a compromise yields access to session cookies, localStorage/localforage caches of record data, and the ability to issue authenticated first-party API calls on the victim's behalf.
  • Untrusted Input Handling: Yes. Strings that reach jQuery APIs originate from (a) AppSheet app definitions authored by customers — column names, view names, action labels, format rules, expressions; (b) end-user record data from customer spreadsheets/databases, including free-text and Long-Text columns and uploaded file names; (c) first-party API responses (JeeneeBrowserAPI.ts, JSONHelp.ts) whose payloads are derived from (a) and (b); and (d) URL/hash and postMessage-adjacent values in the legacy controllers. None of these are trustworthy: an AppSheet app author is a different, lower-privileged principal than the end users of the app they publish.
  • Business Value: jQuery is load-bearing for AppSheet‘s entire legacy view layer and the app editor shell; it cannot be trivially removed. It is also the specific reason this fork exists — the patches were required so AppSheet could roll out require-trusted-types-for 'script' (see go/appsheet-tt-oss). A regression in these patches either breaks the Trusted Types rollout (losing AppSheet’s primary DOM-XSS backstop) or breaks app rendering for all customers. A DOM-XSS in this library is a cross-customer, cross-tenant account-takeover primitive.

Scanning Harness Prompts

Direct the agentic scanner as follows. Note that the only Google-authored code in this repository is the Trusted Types patch set; findings in that delta are strictly higher priority than findings in pristine upstream code.

  1. Audit the Trusted Types patch delta first. Review the four TT-gated branches in src/core/support.js, src/manipulation/support.js, and external/sizzle/dist/sizzle.js. Verify that (a) the window.trustedTypes && window.trustedTypes.createPolicy guard is a sound proxy for the conditions the original probes tested, (b) hardcoding support.noCloneChecked = true and support.option = true does not cause jQuery.fn.clone / wrapMap to skip a correctness or safety step, and (c) skipping the Sizzle addHandle( "type|href|height|width" ) and addHandle( "value" ) registrations does not change how attribute selectors resolve href/src/type/value on any supported browser.
  2. Flag the support.createHTMLDocument type deviation. In the TT branch of src/core/support.js the IIFE is not invoked — support.createHTMLDocument is assigned a function object, where upstream assigns a boolean. Its sole consumer, jQuery.parseHTML in src/core/parseHTML.js, uses a truthy check, so the safe inert-document.implementation.createHTMLDocument branch is still taken; but confirm no other code (including jQuery plugins such as jqueryFileTree, jquery.exif, jquery.canvasresize, or jquery-migrate) performs a strict === true comparison that would silently fall through to parsing untrusted HTML in the live document, which would execute inline event handlers and load subresources immediately.
  3. Trace all string→DOM parsing sinks for XSS. Focus on src/manipulation/buildFragment.js (tmp.innerHTML = wrap[1] + jQuery.htmlPrefilter( elem ) + wrap[2]), jQuery.fn.html (src/manipulation.js, including the rnoInnerhtml shortcut and the try { elem.innerHTML = value } fallback), jQuery.htmlPrefilter, jQuery.parseHTML, and the HTML-string branch of jQuery.fn.init (src/core/init.js, rquickExpr). Confirm the trac-9521 / trac-11290 mitigations (leading-< requirement, #id prioritization) are intact and that no wrapper-map path can be used to smuggle <script>, <style>, <svg>, <math>, or mXSS-style mutation payloads. Explicitly re-verify the CVE-2020-11022 / CVE-2020-11023 htmlPrefilter remediations are present.
  4. Trace all dynamic script-execution sinks. jQuery.globalEval (src/core.js), DOMEval (src/core/DOMEval.js — creates a <script> element, copies type/src/nonce/noModule, appends to doc.head), src/manipulation/_evalUrl.js, the "text script" converter in src/ajax/script.js, and JSONP callback-name handling in src/ajax/jsonp.js (rjsonp, jsonpCallback, the window[ callbackName ] assignment). Pay particular attention to whether DOMEval‘s nonce propagation can be abused to launder attacker-controlled script past AppSheet’s script-src 'nonce-…' 'strict-dynamic' CSP.
  5. Analyze the Sizzle selector parser for injection and ReDoS. external/sizzle/dist/sizzle.js tokenizer regexes (rcomma, rcombinators, rattributeQuotes, matchExpr, rpseudo, runescape) and the :has(), :not(), :contains(), and attribute-selector handlers, for catastrophic backtracking and for cases where a selector string containing < is reinterpreted as HTML by jQuery.fn.init.
  6. Check jQuery.extend prototype pollution. src/core.js contains the CVE-2019-11358 fix (if ( name === "__proto__" || target === copy ) continue;). Verify it is present and complete in both src/ and the committed dist/jquery.js, and that jQuery.isPlainObject / jQuery.parseJSON-adjacent paths cannot reach constructor.prototype.
  7. Verify dist/ provenance. The committed dist/jquery.js / jquery.min.js / jquery.min.map were built locally (banner 3.6.2-pre) and are the artifacts AppSheet actually ships. Diff them against src/ + external/sizzle/ to confirm they contain exactly the TT patch set and no unreviewed additional code.
  8. Report upstream version drift. Baseline 3.6.2 is materially behind current upstream jQuery (3.7.x). Enumerate upstream security-relevant fixes and advisories landed after 3.6.2 that are absent here.

Entry Points and Untrusted Inputs

All entry points are client-side JavaScript API calls made by AppSheet frontend code; the library has no network listeners of its own. “Trusted?” describes the data that AppSheet actually passes.

Entry PointTypeTrusted?Validation
jQuery() / $( selector ) (src/core/init.js, jQuery.fn.init)Selector string or HTML string → DOMNorquickExpr requires a leading < (and trailing >, length ≥ 3) to treat input as HTML; prioritizes #id over <tag> to blunt location.hash XSS (trac-9521). Anything else is handed to Sizzle as a selector.
jQuery.fn.html( value ) (src/manipulation.js)HTML string → innerHTMLNornoInnerhtml = /<script|<style|<link/i gates the direct-innerHTML fast path; jQuery.htmlPrefilter normalizes self-closing tags. No sanitization.
jQuery.fn.append / prepend / after / before / replaceWith / wrap (domManip → buildFragment)HTML string or node → DOMNorhtml = /<|&#?\w+;/ decides string-vs-text; HTML strings go to buildFragment and are parsed via innerHTML. No sanitization.
jQuery.parseHTML( data, context, keepScripts ) (src/core/parseHTML.js)HTML string → detached nodesNoParses into an inert document.implementation.createHTMLDocument( "" ) when support.createHTMLDocument is truthy (so scripts/inline handlers do not auto-run); strips <script> unless keepScripts. Note jQuery.fn.init calls it with keepScripts = true.
jQuery.globalEval( code, options, doc ) (src/core.js → src/core/DOMEval.js)Arbitrary JS stringNoNone. Injects a <script> into doc.head, propagating type/src/nonce/noModule. Direct code-execution primitive.
jQuery.ajax / $.get / $.post / $.getJSON (src/ajax.js, src/ajax/xhr.js)URL, headers, body, responseNoURL is not validated or scheme-checked; crossDomain is inferred by comparing against src/ajax/var/location.js. Response is run through converters selected by dataType/Content-Type.
dataType: "script" responses (src/ajax/script.js)Remote response bodyNoAuto-execution suppressed for cross-domain responses without an explicit dataType; otherwise the "text script" converter calls jQuery.globalEval.
JSONP requests (src/ajax/jsonp.js)Remote response body + callback nameNoResponse is executed as script by design; jsonpCallback name is written to window.
jQuery.fn.load( url, params, callback ) (src/ajax/load.js)Remote HTML fragmentNoResponse is passed to jQuery.parseHTML and injected via .append(...).find( selector ).
jQuery.fn.attr / prop / css / addClass (src/attributes/*.js, src/css.js)Attribute / style valuesNoNo URL-scheme validation — a dynamic href/src value of javascript: or data: is written verbatim.
jQuery.extend( true, target, src ) (src/core.js)Object graph (often parsed JSON)NoSkips __proto__ and self-referential copies (CVE-2019-11358 fix).
Sizzle selector strings (external/sizzle/dist/sizzle.js)CSS selectorNoRegex tokenizer with an internal compile cache; unrecognized pseudo-selectors raise a syntax error.
window.trustedTypes (src/core/support.js, src/manipulation/support.js, sizzle.js)Browser global / feature flagYes (browser-provided)Presence check only (window.trustedTypes && window.trustedTypes.createPolicy). Determines which of the two behavior branches this fork executes.

Trust Boundaries and Auth Assumptions

  • Authentication: None at the library level. jQuery executes in the browser with whatever ambient authority the AppSheet origin already has — session cookies are attached automatically to same-origin jQuery.ajax calls (xhrFields/withCredentials semantics are decided by the caller).
  • Authorization: None at the library level. Access control is enforced server-side by AppSheet (jeenee MainServer / ControllerHandler); jQuery will faithfully issue whatever request the calling code constructs.
  • Implicit trust: The library implicitly trusts every string handed to its HTML and script sinks. It provides no sanitizer and makes no attempt to distinguish app-author content from end-user content. AppSheet's frontend is responsible for choosing .text() over .html(), and for routing genuinely-HTML content through dompurify / sanitize-html / safevalues.
  • Trusted Types as the compensating control: This fork exists precisely because AppSheet enforces require-trusted-types-for 'script' (Security/SecurityHeadersFramework/SecurityHeaderConstants.cs; Security/SecurityHeaders/SecurityHeaders.cs defaults to TrustedTypesMode.PartialEnforce, flag-gated by ITrustedTypesEnforcementModeFeatureGate, with a report-only variant for rollout). The client-side 'default' policy in Nirvana/Content/src/app/globals/trusted_types.ts is deny-by-default: its createHTML returns a value only for '', '&', and '&#8203;' and returns null for everything else, so an unreviewed raw-HTML sink throws rather than executing. Critically, the fork‘s patches only remove innerHTML writes from feature detection; **jQuery’s runtime HTML sinks still assign to innerHTML** and therefore still depend on that default policy for safety. Two important corollaries for scanners: (i) the protection is only as strong as the enforcement flag's rollout state — any user or surface where the gate is off, or where CSP is report-only, has no backstop; and (ii) the default policy is a global, so any code path that broadens it (e.g. the named unsafeCreateHtmlPolicy, 'filepicker-fix', or 'jquery-modal-fix' policies elsewhere in jeenee) re-opens the sinks.
  • Boundary crossings: AppSheet app author (customer) authors an app definition → AppSheet backend stores/serves it → end user (a different, often external principal) loads the app in a browser → app-definition strings and record data cross into the same-origin DOM through jQuery → rendered alongside the victim's authenticated session. A second crossing: first-party API responses (JeeneeBrowserAPI.ts, JSONHelp.ts) → jQuery AJAX converters → DOM. A third: third-party origins reachable via dataType: "script" / JSONP → same-origin script execution.

Sensitive Data Paths

Data TypeSourceDestinationProtection
Customer record data & PII (rows, free-text and Long-Text columns, file names)AppSheet backend API (JeeneeBrowserAPI.ts, JSONHelp.ts → $.ajax / $.getJSONUnsigned / $.postJSON)Legacy view controllers → DOM via jQueryHTTPS transport; server-side authz; rendered predominantly through .text() and React, with Trusted Types as the backstop for any raw-HTML path
App-definition metadata (view/column/action labels, format rules)Customer app author → AppSheet backendLegacyViewManager.tsx and legacy controllers → DOMSame-origin rendering; author is a distinct, lower-trust principal from the end user — this is the primary stored-XSS vector into jQuery sinks
Session cookies & auth contextBrowser cookie jarSame-origin jQuery.ajax requests (src/ajax/xhr.js)Ambient same-origin credentials; not read or persisted by jQuery itself
CSP nonce@CspNonce on <script> tagsCopied onto generated <script> elements by DOMEval (preservedScriptAttributes.nonce)Nonce is propagated verbatim from the reference node — a script-execution laundering surface worth explicit review under 'strict-dynamic'
Cloud-storage file listings (path, name)AppSheet server file-listing API via the getTree callbackscripts/_shared/external/jqueryFileTree.js → $( '<li…><a href="#" rel="' + data[i].path + '">' + data[i].name + '</a></li>' ) → .append()Raw HTML string concatenation, no escaping. Mitigated today only because the sole consumer (components/app/FileTree.tsx) is unreferenced, plus Trusted Types
Action button markup (buttonHtml)Would be app-definition-derivedLegacyViewManager.tsx ShowOverlayActionButtons() → .html( actions[i].buttonHtml )Unescaped .html() sink. Currently dead code (no callers, property never assigned) but present in the shipped bundle
Static third-party map plugin sourceBundled constant MarkerWithLabelRawscripts/_shared/lib/GoogleMaps.ts → $.globalEvalWrapped in safevalues scriptSafeByReview; source is a compile-time constant, not attacker-influenced

Privileged Actions

ActionLocationGuard
HTML string parsing into DOM nodesmanipulation/buildFragment.js:buildFragment (tmp.innerHTML = wrap[1] + jQuery.htmlPrefilter( elem ) + wrap[2])None in-library; relies on the Trusted Types 'default' policy and on callers passing safe values
Direct innerHTML assignmentmanipulation.js:jQuery.fn.htmlrnoInnerhtml (/<script|<style|<link/i) gate + try/catch fallback to empty() + append(); no sanitization
Inert-document HTML parsingcore/parseHTML.js:jQuery.parseHTMLUses document.implementation.createHTMLDocument( "" ) when support.createHTMLDocument is truthy; strips <script> unless keepScripts is set
Arbitrary script executioncore/DOMEval.js:DOMEval, reached via core.js:jQuery.globalEval, manipulation/_evalUrl.js, ajax/script.js "text script" converterNone. Propagates nonce from the reference node so injected scripts satisfy a nonce-based CSP
Deferred/injected script re-execution on DOM insertionmanipulation.js:domManip, manipulation/setGlobalEval.js, manipulation/getAll.js (rscriptType check)Only executes nodes whose type matches rscriptType; tracks already-evaluated nodes via dataPriv
Cross-origin network requestsajax.js:jQuery.ajax, ajax/xhr.js, ajax/script.js script-tag transport, ajax/jsonp.jsBrowser SOP/CORS only; no in-library URL allowlist or scheme validation
Global namespace mutationajax/jsonp.js (window[ callbackName ]), jqueryInit.js (window.$ = window.jQuery = $)None
Attribute / property / style writesattributes/attr.js, attributes/prop.js, css.js:jQuery.fn.cssBoolean-attribute and nodeHook normalization only; no URL-scheme validation on href / src / action
Deep object mergecore.js:jQuery.extendSkips __proto__ and self-referential copies (CVE-2019-11358 remediation)
Feature-detection branch selection (fork-specific)core/support.js, manipulation/support.js, external/sizzle/dist/sizzle.jswindow.trustedTypes && window.trustedTypes.createPolicy presence check only

Priority Review Areas

  1. The Trusted Types patch delta itself (src/core/support.js, src/manipulation/support.js, external/sizzle/dist/sizzle.js) — this is the only Google-authored code in the repository, it is unreviewed by upstream, and it changes support.* flags that gate cloning, wrapMap wrapping, and attribute resolution throughout the library.
  2. The support.createHTMLDocument function-vs-boolean deviation — the TT branch assigns an uninvoked IIFE where upstream assigns a boolean; it is truthy so jQuery.parseHTML still takes the safe inert-document path today, but any strict === true comparison in a plugin or a future upstream merge would silently downgrade parsing into the live document, executing inline handlers and firing subresource loads.
  3. HTML-string sinks reachable from customer content — buildFragment/htmlPrefilter, jQuery.fn.html, jQuery.parseHTML, and the HTML branch of jQuery.fn.init; these are the direct stored-DOM-XSS path for app-definition and record data authored by one AppSheet principal and rendered in another's authenticated session.
  4. Script-execution sinks and CSP-nonce laundering — jQuery.globalEval → DOMEval, _evalUrl, the "text script" AJAX converter, and JSONP; the deliberate copying of the nonce attribute onto generated <script> elements is exactly the primitive that defeats a nonce + 'strict-dynamic' policy if an attacker controls the evaluated string.
  5. Selector injection and ReDoS in Sizzle — a hand-rolled regex tokenizer that AppSheet feeds with dynamically constructed selectors (e.g. the window-exposed SubscribeEmail( id ) in components/account/Settings.tsx), where a string beginning with < is silently reinterpreted as HTML.
  6. Prebuilt dist/ provenance — dist/jquery.js/.min.js/.min.map were generated locally (3.6.2-pre, 2023-03-15) rather than taken from an upstream release, and they, not src/, are what webpack bundles and ships to customers; they must be proven to correspond exactly to the reviewed source.
  7. Upstream version drift and absent CVE backports — the fork is frozen at 3.6.2 (Dec 2022) with zero security patches applied on top, so every post-3.6.2 upstream security fix is missing and the pinned-by-SHA package.json dependency will never pick one up automatically.
  8. Known unescaped call sites in AppSheet's own jQuery usage — the data[i].path / data[i].name concatenation in scripts/_shared/external/jqueryFileTree.js and the .html( actions[i].buttonHtml ) call in LegacyViewManager.tsx; both are presently unreachable but ship in the bundle and would become live XSS the moment a caller is added.

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.