blob: a9ac1eedf76fabd3ba1ee85dafd05221858e8ae0 [file] [view]
# 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.