kylefox/jquery-modal v0.9.1; this fork self-identifies as Version 0.9.1-patched in the jquery.modal.js banner).https://gnocchi-internal.googlesource.com/third_party/jquery-modal, branch main).jquery.modal.js and the build artefact jquery.modal.min.js it produces via gulpfile.js (uglify). jquery.modal.css / .min.css, examples/, compositor.json, close.sketch, and CHANGELOG.md carry no executable server or client logic of interest.[!IMPORTANT] This fork reaches production as a vendored copy, not as an npm/bower dependency. No
package.jsonin AppSheet'sjeeneemonorepo declares a dependency onjquery-modal— dependency-graph tooling will therefore report this repository as unused. It is not. The build artefact in this repository is byte-identical (verified by MD5,8e82b0b05201141e034c93709b61f88a) to the file checked into the AppSheet web frontend atjeenee/Nirvana/Content/scripts/_shared/external/jquery-modal-0.9.1.min.js. That file is served to end users. This repository is the confirmed build source of live, Internet-facing production JavaScript, and findings here are directly exploitable findings in AppSheet.
Production consumer. The vendored copy is loaded by exactly one view, the AppSheet sign-in / sign-up page (jeenee/V3/MainServer/Views/Account/Login.cshtml):
Login.cshtml:45 — <link rel="stylesheet" href="~/Content/css/jquery-modal-0.9.1.min.css">Login.cshtml:116 — <script nonce="@CspNonce" src="~/Content/scripts/_shared/external/jquery-modal-0.9.1.min.js">Its purpose is to display an individual AppSheet application's Terms of Service and Privacy Policy inline in a modal rather than navigating away from the login flow. Login.cshtml:37-38 construct the two links as /appinfo/termsofuse?appId=<AppId> and /appinfo/privacypolicy?appId=<AppId>; Login.cshtml:39 sets relMode = "modal:open" whenever ViewBag.AppId != null and the client is not iOS (iOS is excluded per b/463902264); Login.cshtml:109-110 render the anchors with rel="@relMode".
Because those href values are not fragment identifiers (#…), the plugin takes its AJAX branch (jquery.modal.js, $.modal constructor, the else arm of the /^#/.test(target) check). The AJAX branch is therefore exercised in production, on an unauthenticated page — it is not dead code.
jquery.modal.min.js is minified JavaScript shipped verbatim to every visitor of the AppSheet login page.innerHTML assignment of a network-fetched HTML response. It also performs unvalidated URL dereferencing driven by DOM attributes.href attribute values from arbitrary anchors bearing rel~="modal:open", and (b) the full HTTP response body returned from dereferencing those URLs, which it injects into the document as HTML without sanitisation.jquery.modal.js, inside the $.get(target).done(...) callback of the $.modal constructor, the fork creates a Trusted Types policy named jquery-modal-fix whose createHTML callback ignores its argument and returns the closed-over html response body verbatim: createHTML: function(_ignored) { return html }, invoked as policy.createHTML('_ignored'). The result is assigned to current.$elm[0].innerHTML. Flag this as a sanitiser-bypass / TT escape hatch: it satisfies the Trusted Types type check while providing zero filtering, so any HTML in the fetched response is injected into the DOM. Confirm the identical construct in the minified artefact jquery.modal.min.js (search for the string jquery-modal-fix).$.modal constructor, target = el.attr('href') is taken directly from the clicked anchor and passed to $.get(target). The only branch condition is /^#/.test(target); there is no scheme allowlist, no same-origin check, no path allowlist. Any anchor anywhere on a page that loads this plugin and carries rel~="modal:open" (bound globally at the bottom of the file via $(document).on('click.modal', 'a[rel~="modal:open"]', ...)) will cause the referenced URL to be fetched and its body injected. Trace whether an attacker can influence any href or inject an anchor with that rel.trustedTypes referenced unguarded. The line current.$elm[0].innerHTML = trustedTypes.emptyHTML; dereferences the bare global trustedTypes outside the if (window.trustedTypes && …) guard that immediately precedes it. In a browser without Trusted Types this throws a ReferenceError inside the jQuery Deferred callback. Assess this as a correctness/availability defect and check whether the resulting partially initialised modal state (modals array already pushed, spinner still mounted) is exploitable or merely a broken UI.safeHtml (AJAX callback) and txt (show()) are assigned without var/let, creating window-scoped globals. Check for cross-modal state leakage and clobbering of same-named globals on the host page.show(), the element is built into the local closeButton, but the append uses this.$elm.append(this.closeButton) — this.closeButton is never assigned, so the close control is never added and hide()'s if (this.closeButton) this.closeButton.remove() is dead. Verify whether losing the close affordance forces users into the escape/overlay-click paths and whether any modal is rendered unclosable.89f9a94, df04d26 and 07569a3 rewrote showSpinner(), show() and block() to use document.createElement / setAttribute / createTextNode in place of jQuery HTML-string construction. Verify no residual HTML-string sink remains (the spinnerHtml entry in $.modal.defaults is now vestigial and unused — confirm it is genuinely unreferenced).| Entry Point | Type | Trusted? | Validation |
|---|---|---|---|
a[rel~="modal:open"] global click delegate (bottom of jquery.modal.js) | DOM event | No — matches any anchor in the document | None. Any matching anchor triggers $(this).modal() |
$.modal constructor, target = el.attr('href') | DOM attribute | No | Only /^#/ is special-cased (same-document element). Everything else is fetched |
$.get(target) response body (html) | HTTP response | No | None. Passed through the no-op jquery-modal-fix TT policy and assigned to innerHTML |
$.modal.defaults overrides via $(el).modal(options) | JS API | Partially — caller-supplied | closeText, closeClass, modalClass, blockerClass flow into createTextNode / setAttribute, so they are text/attribute-safe, not HTML-safe |
keydown (Esc) and blocker click handlers | DOM event | N/A | Only invoke close() |
Login.cshtml is the pre-auth sign-in/sign-up surface, so the plugin runs in an anonymous-origin context.href on a rel="modal:open" anchor is a trustworthy, first-party URL, and (b) the HTML body returned from that URL is safe to inject. Neither is enforced in code. In the current production wiring, the URL is server-constructed (/appinfo/{termsofuse,privacypolicy}?appId=<AppId>) and same-origin, so the assumption happens to hold today — but nothing in the library preserves it, and the injected content is app-owner-authored Terms/Privacy text rendered by an AppSheet endpoint, i.e. content originating from a third party (the app creator) rather than from Google.GET /appinfo/* → response body injected as live HTML into the login page DOM. The last hop is the trust boundary that this fork removes the guard from.Login.cshtml emits every <script> tag with nonce="@CspNonce", which indicates a nonce-based Content Security Policy is enforced on this page. A correctly configured nonce CSP would block <script> elements injected via innerHTML (injected nodes carry no valid nonce) and, in most modern browsers, innerHTML does not execute inline <script> at all.
This threat model therefore does not claim unmitigated script execution. What the jquery-modal-fix policy demonstrably does is:
<script> content that CSP does not necessarily cover: attacker-controlled markup, styling, overlaid forms, and links on the credential-entry page — i.e. in-page phishing / UI redress against users mid-login;unsafe-inline fallback, or the page is rendered in a context where the nonce is absent.A reviewer should independently verify the effective CSP response headers on the /Account/Login route — the nonce attribute in the view proves a nonce is minted, not that script-src lacks unsafe-inline or a permissive fallback.
| Data Type | Source | Destination | Protection |
|---|---|---|---|
Application ID (appId) | ViewBag.AppId → anchor href (Login.cshtml:37-38) | $.get(target) query string | Server-constructed; not user-supplied at the client layer |
| App Terms of Service / Privacy Policy HTML | GET /appinfo/{termsofuse,privacypolicy} — content authored by the app creator, a third party relative to Google | current.$elm[0].innerHTML on the login page | None. No sanitisation; the TT policy is a pass-through |
| Login page DOM (credential fields, federated-identity buttons, CSRF tokens) | AppSheet server-rendered | Shares a document with the injected HTML | Same-origin isolation only; no iframe/sandbox boundary between injected content and the credential form |
| Action | Location | Guard |
|---|---|---|
| Create a Trusted Types policy that returns unsanitised HTML | jquery.modal.js : $.modal constructor, $.get().done() callback — policy name jquery-modal-fix | None. createHTML discards its input and returns the fetched body verbatim |
Assign untrusted HTML to innerHTML | jquery.modal.js : $.modal constructor, $.get().done() callback (current.$elm[0].innerHTML = safeHtml) | Trusted Types type check only — satisfied by the no-op policy above |
Outbound HTTP GET to a DOM-supplied URL | jquery.modal.js : $.modal constructor ($.get(target)) | Only /^#/ diverts to the local-element path; no scheme or origin allowlist |
DOM mutation: append blocker/spinner/modal to <body> | $.modal.prototype.block, .showSpinner, .show | Element-API construction (createElement/setAttribute/createTextNode) — no HTML strings |
Global document event binding (click.modal, keydown.modal) | Module IIFE tail; $.modal.prototype.open | Delegated on document; applies to every anchor on any page loading the plugin |
jquery-modal-fix Trusted Types escape hatch ($.get().done() in jquery.modal.js). A Trusted Types policy whose createHTML ignores its argument and returns a network-fetched response body verbatim is a sanitiser bypass by construction. It converts the fetched HTML into a TrustedHTML that passes the browser's TT enforcement while providing no filtering whatsoever, and the result is written straight to innerHTML on the public, unauthenticated login page. This nullifies Trusted Types on the one path that actually needed it and should be replaced with a real sanitiser (e.g. DOMPurify with RETURN_TRUSTED_TYPE: true) or with element-API construction. Highest priority.href → $.get flow ($.modal constructor). The /^#/ test is the sole branch discriminator. Combined with the global a[rel~="modal:open"] delegate, any injected or attacker-influenced anchor becomes a fetch-and-inject primitive. Review for an allowlist of same-origin, path-prefixed URLs (e.g. /appinfo/)./Account/Login, and the blast radius if it is weaker than assumed. Verify the deployed script-src directive. Even with a strict nonce CSP, enumerate the non-script injection consequences on a credential-entry page: overlaid fake login forms, <a>/<form> action rewriting, and CSS-based UI redress./appinfo/termsofuse and /appinfo/privacypolicy output. These endpoints render app-creator-authored content. Determine server-side whether that content is sanitised before it leaves the server; if it is not, the missing client-side sanitisation in this fork is the only thing standing between an app creator and injected markup on AppSheet's login page for every user of their app.trustedTypes.emptyHTML dereference and implicit globals (safeHtml, txt). Correctness and state-consistency defects introduced by the patch series; assess for exploitable partial-initialisation states and global clobbering.jquery.modal.min.js is a faithful minification of jquery.modal.js at every commit. The minified file is hand-committed (8fe155c "Creating the patched minified file"), not produced by CI, so the shipped artefact could silently diverge from the reviewed source.examples/, compositor.json, close.sketch, CHANGELOG.md, README.md, bower.json, LICENSE — documentation, design assets, and packaging metadata with no production code path.gulpfile.js and its devDependencies (gulp 3.x and transitive packages, with a graceful-fs ^4.2.11 override) — build-time only; this toolchain is not run in CI for this repository and produces no served artefact automatically.jquery.modal.css / jquery.modal.min.css — stylesheets with no scripting surface.jQuery, but that dependency is satisfied by AppSheet's own separately vendored jQuery build, which is tracked as its own Fortify target (gnocchi-internal/third_party/jquery, b/548455274)./appinfo/* endpoints, and the CSP middleware that sets @CspNonce. Both live in the jeenee repository and must be reviewed there; they are referenced here only as context and as mitigating controls to be verified.