Security Threat Model: @protobufjs/inquire (AppSheet Fork)

Asset Definition & Scope

@protobufjs/inquire is a ~25-line JavaScript shim (repository: sso://gnocchi-internal/third_party/@protobufjs/inquire, branch main; mirrored at appsheet-third-party/@protobufjs/inquire). It exports exactly one function, inquire(moduleName), whose entire purpose is to perform a guarded, bundler-invisible require() of an optional CommonJS module and return null instead of throwing when that module is absent.

  • Upstream: protobufjs/protobuf.js → lib/inquire, npm version 1.1.0, upstream revision 56b1e64979dae757b67a21d326e16acee39f2267 (last_upgrade_date 2023-03-27, per METADATA).
  • Scope: index.js (the single implementation file) and index.d.ts. Everything else in the repository is packaging (package.json, LICENSE, README.md, METADATA) or upstream tape tests under tests/.
  • Deployment context: Bundled into the AppSheet web frontend (jeenee/Nirvana/Content) and executed in the end user's browser. It is a transitive runtime dependency of protobufjs@7.4.0; the AppSheet package.json also pins it as a direct dependency purely to force npm to resolve every copy in the tree to this fork.

[!IMPORTANT] This is a genuinely tiny library with no untrusted input surface. Its aggregate risk is Low. It is nevertheless worth scanning because the implementation contains two eval() calls, which makes it a high-noise target for static analysers and a plausible supply-chain gadget if a future caller ever passes a non-literal module name.

The AppSheet Customization

The repository has a single commit — 9f141da4daaf73c32cb78a8bcf38333870e80cbc (“gnitial import of @protobuf/inquire”) — which already contains the Google patch. Diffing index.js against upstream 1.1.0 shows the fork wraps the original eval-based require in a Trusted Types branch:

function inquire(moduleName) {
    try {
        if (typeof self !== 'undefined' && self.trustedTypes && self.trustedTypes.createPolicy) {
            const escapeScriptPolicy = trustedTypes.createPolicy("myEscapePolicy", {
                createScript: (string) => "require " + string,
            });
            safeScript = escapeScriptPolicy.createScript(moduleName);   // <-- undeclared
            var mod = eval(safeScript);
        } else {
            var mod = eval("quire".replace(/^/,"re"))(moduleName);      // upstream path
        }
        if (mod && (mod.length || Object.keys(mod).length))
            return mod;
    } catch (e) {}
    return null;
}

Two independent defects in the new branch mean the dynamic require is effectively dead code in every Trusted-Types-capable browser (Chrome, Edge):

  1. safeScript is assigned without var/let/const. index.js is under "use strict", so the assignment throws ReferenceError: safeScript is not defined before eval is ever reached.
  2. Even if safeScript were declared, the policy's createScript produces the string "require " + moduleName, which is not valid JavaScript (require foo is a SyntaxError, not a call expression).

Either fault is swallowed by the bare catch (e) {} and inquire() returns null. protobufjs treats null as “optional module unavailable” and falls back to its pure-JS Long/Buffer code paths, so behaviour is correct — the patch neutralises the dynamic require rather than sanitising it. The legacy else branch retains the original upstream eval("quire".replace(/^/,"re")) obfuscated require for non-TT environments (Node, Safari, jsdom under Jest).

Reachability and Call Sites

inquire() is invoked from exactly two places in the dependency graph, both in protobufjs/src/util/minimal.js, and both with a string literal:

CallerArgument
util.Buffer initialisationutil.inquire("buffer")
util.Long initialisationutil.inquire("long")

There is no path by which a caller — let alone customer-authored app data — supplies the moduleName argument. Arbitrary-module-load / dynamic-require abuse is not reachable in the current AppSheet integration.

Prioritization Signals

  • 1P OSS: No (Third-party OSS maintained as an internal customized fork for Google AppSheet)
  • 1P Proprietary Shipped Software: Yes (Minified into the AppSheet frontend JavaScript bundles shipped to browsers)
  • High-Risk Code Surface: Yes, but narrowly — the file contains eval() and constructs a Trusted Types policy, both intrinsically dangerous primitives (CWE-95, CWE-676). The surface is 25 lines with no parsing, no I/O, and no data handling.
  • Perimeter Exposure: Yes (AppSheet is a 1P service exposed to the public Internet and its customers; this code executes in every end user's browser session that loads a protobuf-backed page)
  • Data Sensitivity: Low (the module handles no data — its only argument at every call site is a hardcoded module name, and its return value is a module object, never user content)
  • Untrusted Input Handling: No (customer-authored app data never reaches inquire; the only inputs are the literals "buffer" and "long")
  • Business Value: Low-severity but non-zero: it is a mandatory transitive dependency of protobufjs, which the AppSheet frontend uses for wire (de)serialization. A regression that made moduleName caller-controlled, or that leaked the Trusted Types policy object, would upgrade this from a dormant eval to an exploitable script-injection sink on a public-Internet-facing surface.

Scanning Harness Prompts

  1. Dynamic require / arbitrary module load (primary question). Confirm that inquire(moduleName) in index.js is only ever reached with compile-time string constants. Flag any call site — inside this repo, in protobufjs, or in AppSheet application code — where moduleName derives from a variable, a property lookup, a network response, location.*, or customer-authored app configuration. That, and only that, converts these eval() calls into an arbitrary-code-execution primitive.
  2. Trusted Types policy hygiene. Review the trustedTypes.createPolicy("myEscapePolicy", { createScript: ... }) call in index.js:inquire. Verify (a) the policy object stays a function-local const and is never exported, attached to window, or otherwise reachable by other page scripts — a reachable permissive createScript policy is a universal Trusted Types bypass gadget; (b) the policy name is scoped in the page CSP trusted-types allowlist and does not shadow default; (c) repeated invocation (duplicate policy name) fails closed via the existing try/catch rather than throwing out of the module.
  3. Both eval() sinks. eval(safeScript) and eval("quire".replace(/^/,"re"))(moduleName). The replace is deliberate upstream obfuscation to hide the identifier from webpack/browserify static analysis; confirm no additional obfuscated indirection has been introduced and that neither sink can be reached with attacker-influenced text.
  4. Silent failure semantics. The bare catch (e) {} swallows every error, including the strict-mode ReferenceError documented above. Verify that downstream protobufjs code correctly handles a null return (it does today: it falls back to pure-JS Long/Buffer), so the neutralised require cannot cause an unhandled exception, an infinite retry, or a silent correctness bug in message decoding.
  5. Fork drift. If this fork is ever re-synced with upstream protobufjs, re-verify that the Trusted Types branch survives and that the strict-mode safeScript behaviour (fail-closed to null) is preserved or deliberately replaced with an equally closed implementation.

Entry Points and Untrusted Inputs

Entry PointTypeTrusted?Validation
inquire(moduleName) (index.js)Exported function call, in-processYes — every known call site passes a hardcoded literal ("buffer", "long") from protobufjs/src/util/minimal.jsNone. The value is passed straight into a Trusted Types createScript or into the obfuscated require. Safety rests entirely on the caller contract, not on validation.
self.trustedTypes (index.js)Ambient browser global / feature detectionYes (user-agent supplied)Guarded with typeof self !== 'undefined' && self.trustedTypes && self.trustedTypes.createPolicy before use
Resolved module object (mod)Result of require/evalYes (bundle-resolved module, not user data)Emptiness check only: mod && (mod.length || Object.keys(mod).length)

Trust Boundaries and Auth Assumptions

  • Authentication: None. This is an in-process library function with no network, IPC, or filesystem interface.
  • Authorization: None. Any code already executing in the bundle can call inquire; there is nothing to authorise against.
  • Implicit trust: The library implicitly trusts its caller to supply a static, non-attacker-influenced module name. This is the single load-bearing assumption in the entire threat model.
  • Boundary crossings: None at the library level. The only boundary of note is the browser's Trusted Types / CSP boundary, which the fork touches by minting a createScript policy — the code moves toward that boundary but, because of the strict-mode fault, never actually crosses it.

Sensitive Data Paths

Data TypeSourceDestinationProtection
Module name stringHardcoded literals in protobufjs/src/util/minimal.js ("buffer", "long")Trusted Types createScript → eval, or obfuscated require()Not sensitive; not user-derived. No sanitisation applied or required at present.
Resolved module objectBundler module registryprotobufjs util.Buffer / util.LongEmptiness check only; returned to caller by reference

No secrets, credentials, tokens, PII, or customer app data pass through this module.

Privileged Actions

ActionLocationGuard
Dynamic script evaluation (eval of a TrustedScript)index.js:inquire (Trusted Types branch)Trusted Types createScript policy; additionally fails closed via strict-mode ReferenceError on the undeclared safeScript, caught by the surrounding try/catch
Dynamic module load (obfuscated eval("quire".replace(/^/,"re"))(moduleName))index.js:inquire (legacy / non-TT branch)None beyond the caller contract that moduleName is a literal; failure is swallowed by catch (e) {}
Trusted Types policy creation (trustedTypes.createPolicy("myEscapePolicy", …))index.js:inquireFeature-detection guard only. Policy name must be permitted by the page's CSP trusted-types directive; the policy object is a function-local const and is not exported.

Priority Review Areas

  1. Caller-controlled moduleName (highest value, lowest current likelihood). The one question that matters for this library is whether the require path can ever be attacker-influenced. Today it cannot: the only two call sites pass "buffer" and "long". Scanning should be tuned to alert loudly on any new dynamic argument rather than on the mere presence of eval.
  2. Trusted Types policy leakage in inquire. A permissive createScript policy that escapes function scope would be a page-wide Trusted Types bypass — far more severe than anything else in this repository. Confirm it stays local and that "myEscapePolicy" is intentional in the AppSheet CSP allowlist (go/trusted-types-appsheet).
  3. Correctness of the fail-closed patch. The Google patch neutralises the dynamic require through two accidental-looking faults (undeclared safeScript under "use strict", and a createScript template that produces invalid JavaScript). This is the desired security outcome but it is fragile: a well-meaning cleanup that adds var safeScript and fixes the template would re-enable eval-based dynamic require in the browser. Treat any change to these lines as security-relevant and require the fail-closed behaviour to be made explicit (e.g. return null;) rather than incidental.
  4. Bare catch (e) {} error swallowing. Low severity, but it hides the permanent ReferenceError and would equally hide a genuine CSP/Trusted Types violation or a bundler resolution failure, delaying detection of a regression in area 3.

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.