Repository navigation
build: run npm dependencies under Hardened JavaScript, pin the host globals and disable install scripts - #1642
Open
juanky201271 wants to merge 7 commits into
Open
juanky201271 wants to merge 7 commits into
juanky201271 wants to merge 7 commits into
Conversation
juanky201271
force-pushed
the
lavamoat/1-allow-scripts
branch
2 times, most recently
from
October 20, 2026 15:46
f96ff10 to
90e317c
Compare
zancas
force-pushed
the
lavamoat/1-allow-scripts
branch
from
November 8, 2026 22:49
e620b5d to
4a5759a
Compare
`yarn` no longer runs any dependency's preinstall/install/postinstall. `yarn setup` installs with the frozen lockfile and then runs `allow-scripts`, which executes only the scripts allowed under lavamoat.allowScripts plus our own postinstall (patch-package). Every package that currently ships an install script (esbuild, unrs-resolver, @parcel/watcher) works without it because the prebuilt binaries arrive as optional dependencies, so the allowlist starts empty. @lavamoat/preinstall-always-fail makes a lost .yarnrc fail the install instead of silently re-enabling scripts. CI and the developer quickstarts use `yarn setup`. Refs #1641 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The entry landed after skip-missing, which breaks the YAML. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The SES shim for Hermes loads as the first Metro polyfill and repairs the intrinsics; after InitializeCore and before the entry file, `hardenIntrinsics()` freezes Object, Array, Promise, JSON, Function prototypes and the rest, so no dependency can patch them once the app runs. Prototype pollution and intrinsic hooking from a compromised npm package stop working. Hermes has no Compartments, so this is lockdown only: a dependency keeps direct access to whatever it can reach, it just cannot reach the rest of the app through the language builtins. The lockdown options are the package's: console and error taming off, so the release console silencing in index.js keeps working and stack traces stay complete. Babel ignores the shim so it reaches Hermes untouched. Refs #1641 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lockdown() freezes the language intrinsics but leaves globalThis writable, and fetch, XMLHttpRequest, WebSocket and the timers are React Native's globals, not the language's. A dependency could still replace them. The first import of index.js makes their bindings non-writable and non-configurable, once InitializeCore has installed them and before any other module evaluates. Bindings only: the prototypes behind them stay writable because the debug network inspector patches them. react-native-worklets reassigns timers, but on the UI and worklet runtimes, which have their own globalThis. Refs #1641 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A plain yarn add re-links node_modules without the lifecycle scripts, so the patches are gone until allow-scripts runs again. Before ignore-scripts our postinstall did that on its own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The React Native Babel preset emits sloppy-mode modules, where a write to a non-writable data property completes as a silent no-op. The pin now defines each host global as a non-configurable accessor whose getter returns the value React Native installed and whose setter throws, in debug and release alike. SES gives the intrinsics the same form under severe override taming. The pinned set follows one rule: every host global that setUpXHR and setUpTimers install, minus the two renderer-private microtask names. File, FileReader, requestIdleCallback and cancelIdleCallback join the list, and a test derives the set from those two modules in both bridge modes. The pin reads the own descriptor, which leaves an inherited binding alone and skips its own accessor on a second run. CONTEXT.md gains the Hardened runtime vocabulary ruled on 2026-11-08. The notifee uninstall moves to its own branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The existing write test runs inside a Jest module, where the module mode decides whether a data pin throws or completes the write as a silent no-op. The new test writes through Reflect.set, which hands back false on a non-writable data property in every module mode and calls the setter on an accessor, and expects the TypeError that setter throws. Against a data pin the test fails, which is the case the ruling of 2026-11-08 is about. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
zancas
force-pushed
the
lavamoat/1-allow-scripts
branch
from
November 10, 2026 04:49
7f302ba to
6d0792b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The LavaMoat arc of #1641 in one PR: phase 1 (allow-scripts), phase 2 (SES lockdown) and phase 3 (pinned host globals). #1646 and #1652 carried phases 2 and 3 and were merged into this branch on 2026-11-10. The rulings of the grilling session of 2026-11-08 are in, the vocabulary is in the "Hardened runtime" section of CONTEXT.md, and ADR zingo-mobile/0021 (zingolabs/zingo-adrs#44) is amended in zingolabs/zingo-adrs#66.
Phase 1: no install scripts
.yarnrcsetsignore-scripts true.yarnruns nopreinstall,installorpostinstallof any dependency.yarn setupisyarn install --frozen-lockfile && yarn allow-scripts.allow-scriptsruns only the packages allowed underlavamoat.allowScriptsinpackage.json, plus our ownpostinstall(patch-package).yarn setup. The quickstarts say to runyarn allow-scriptsagain after a plainyarn addoryarn remove.@lavamoat/preinstall-always-failis a dev dependency whose one job is to fail the install when the.yarnrcentry is lost.falsein the allowlist:esbuild(via tsx and vite),unrs-resolverand@parcel/watcher(via jest) take their binaries from optional platform packages. A new dependency with an install script failsallow-scriptsuntil it is added on purpose (yarn allow-scripts autoadds it asfalse).Phase 2: lockdown
@lavamoat/react-native-lockdown3.0.0 (ses 2.3.0) is theserializerinmetro.config.js. The SES shim for Hermes becomes the first Metro polyfill and repairs the intrinsics. After InitializeCore and before the entry file,hardenIntrinsics()freezesObject,Array,Promise,JSONandFunction.prototype. No dependency can patch them once the app runs.babel.config.jsignoresses.cjsandses-hermes.cjs, and the shim reaches Hermes untouched. The pattern accepts backslashes because Metro hands Babel Windows paths.errorTamingandconsoleTamingare off, which keeps the releaseconsole.logsilencing inindex.js(Audit Issue K) and complete stack traces.overrideTamingissevere, which keeps assignment to.constructorand.toStringon objects that inherit from frozen prototypes.evalfor SES (Secure EcmaScript) support meta/hermes#957). This is lockdown only, with no per-package isolation.Phase 3: pinned host globals
lockdown()freezes the language intrinsics and leavesglobalThiswritable.fetch,XMLHttpRequest,WebSocketand the timers are React Native globals. After phase 2 alone a dependency can still runglobal.fetch = eviland read every request. That was one half of thechalkanddebugcompromise of September 2025.app/utils/pinGlobals.tsdefines each host global as a non-configurable accessor onglobalThis. The getter returns the value React Native installed. The setter throws aTypeErrorthat names the global, in debug and release alike. The React Native Babel preset emits sloppy-mode modules, where a write to a non-writable data property completes as a silent no-op. The accessor removes that case, the same way SES removes it for the intrinsics underoverrideTaming: severe.setUpXHRandsetUpTimersinstall, minus the two renderer-private microtask names. The list holds 25 names, among themFile,FileReader,requestIdleCallbackandcancelIdleCallback. A test derives the set from those two React Native modules in both bridge modes.app/hardenRuntime.tsis the first import ofindex.js. InitializeCore has run andhardenIntrinsics()has frozen the language. Nothing else has evaluated.XMLHttpRequest.prototypeandWebSocket.prototypestay writable in this PR. Prototype hardening in both builds is the fourth step of the arc and its own PR. It gives up the in-app Network Inspector of the dev menu, which rewrites that prototype when toggled.Checked
node_modulesandyarn install --frozen-lockfile:tsx,esbuild,unrs-resolverand@parcel/watcherall load, andreact-native-fsis patched only afteryarn allow-scripts.react-native bundle --dev false). The SES shim sits at the top of the bundle with zero Babel helpers in its region. The run order at the end is InitializeCore, thenhardenIntrinsics(), then the entry module, whose first require ishardenRuntime.hermesc -O -emit-binarycompiles the bundle without errors and LavaMoat reports no anomaly.X.prototype.y =anddefineProperty(X.prototype, ...)findsjs-base64(the opt-inextendString(), which the app never calls) andrecyclerlistview(its own class). Nothing patches an intrinsic after the freeze.react-native-workletsreassignssetTimeout,requestAnimationFrameandqueueMicrotaskinsideexecuteOnUIRuntimeSync, on the worklet runtimes, which hold their ownglobalThis. The React Native runtime copy is untouched.__tests__/pinGlobals.unit.tsholds six tests: a write throws, a write that discards the [[Set]] result throws as a sloppy-mode module's would, a lazy getter resolves once, a second run keeps rejecting writes, an inherited binding stays on its prototype, and the pinned set equals what React Native installs.typecheck,eslint,prettier,depcheckand Jest are green. The CI run on the phase 3 head passed, with the 40 offline device tests green under the pin.package.jsonandyarn.lock, where the@lavamoatentries sit beside dev's@playwright/testbump, andyarn install --frozen-lockfileaccepts the lockfile.Needs a device run
pinGlobals()once with aperformance.now()pair, debug and release, and report the two numbers here against the 5 ms budget.TypeError: Cannot assign to read only property <name>naming the module. The fix is to drop the package or to add its shim togetPolyfillsbefore the freeze. A pin failure surfaces asTypeError: <name> is a Pinned Global and rejects writes after the entry file, or asTypeError: Cannot redefine property: <name>when anything callspolyfillGlobalafter the entry file.bench:memoryfor the startup cost, the Maestro nightly and the visual capture in CI.STORYBOOK_ENABLED=true).withStorybookleaves the serializer alone, and one run confirms it.Known limits
🤖 Generated with Claude Code