| # 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: |
| |
| ```js |
| 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**: |
| |
| | Caller | Argument | |
| |---|---| |
| | `util.Buffer` initialisation | `util.inquire("buffer")` | |
| | `util.Long` initialisation | `util.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 Point | Type | Trusted? | Validation | |
| |---|---|---|---| |
| | `inquire(moduleName)` (`index.js`) | Exported function call, in-process | Yes — every known call site passes a hardcoded literal (`"buffer"`, `"long"`) from `protobufjs/src/util/minimal.js` | None. 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 detection | Yes (user-agent supplied) | Guarded with `typeof self !== 'undefined' && self.trustedTypes && self.trustedTypes.createPolicy` before use | |
| | Resolved module object (`mod`) | Result of `require`/`eval` | Yes (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 Type | Source | Destination | Protection | |
| |---|---|---|---| |
| | Module name string | Hardcoded 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 object | Bundler module registry | `protobufjs` `util.Buffer` / `util.Long` | Emptiness check only; returned to caller by reference | |
| |
| No secrets, credentials, tokens, PII, or customer app data pass through this |
| module. |
| |
| ## Privileged Actions |
| |
| | Action | Location | Guard | |
| |---|---|---| |
| | 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:inquire` | Feature-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. |