[APPS-2792] Harden network-guard, drop local network blocking - #524
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
bf884e8 to
af21eb5
Compare
Live Debugger Runtime BenchmarkSDK-loaded dormant-probe runtime overhead, measured against an uninstrumented bundle in the same browser session.
What do the Tiny and Hot workloads represent? Browser Debugger SDK: Full diagnosticsRaw samples are in the |
af21eb5 to
7a9975a
Compare
a6e26b1 to
c413714
Compare
b35e776 to
5f94ba9
Compare
| export type LoadModule = (specifier: string) => Promise<Record<string, unknown>>; | ||
|
|
||
| /** Loads a customer module under the same top-level `$`-scoping `runScriptLocally` uses, for callers (like `executeColdActionLocally`'s priming) that evaluate it before `runScriptLocally`. Top-level code runs outside `runBlocked`, so it isn't network-guarded; loading the guard first ensures its trusted stdout/stderr are captured before any customer code can repoint them. */ | ||
| /** Loads a customer module under the same top-level `$`-scoping `runScriptLocally` uses, for callers (like `executeColdActionLocally`'s priming) that evaluate it before `runScriptLocally`. Top-level code runs outside `runBlocked`, so it isn't guarded; loading the guard first ensures its trusted fetch is captured before any customer code can replace `globalThis.fetch`. */ |
There was a problem hiding this comment.
Not changed, deliberately: module top-level code (the customer entry, evaluated by loadCustomerModuleEntry before runBlocked) running unguarded is an accepted gap, stated in network-guard.ts's threat note. Evaluating initialization inside the blocked scope would change when module-level state is created, and on Vite 5 it would also block Vite's own transform work. The guard targets accidental dependency behavior during the function body, not hostile code. Leaving this open for a reviewer to confirm.
🤖 Addressed by Claude Code
5f94ba9 to
7fc5b67
Compare
assertConnectionIdAllowed only checks a call's own top-level connectionId field against the function's allowlist. A call to a platform meta-action whose own inputs carry a separate allowedConnectionIds field (e.g. a nested script-execution call) reached the destination with a self-declared scope this never validated — the local mirror of server-side connection scoping was bypassable by nesting. Rejects any $.Actions call whose inputs declare allowedConnectionIds, on both entry points (raw proxy and action-catalog typed wrapper, which share this validation). The dev server's own submission of this same kind of call goes through a different path (dev-server.ts's makeExecuteActionRemotely), never through $.Actions, so nothing legitimate is affected. The check is own-property, so a polluted Object.prototype can't make every later call falsely appear to declare the key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocesses Local dev always allows real network access (net/fetch/dgram/dns/ WebSocket/EventSource), matching Terrapin's production sandbox, which allows outbound network. The team agreed to allow network locally and roll the Terrapin backend function out behind a flag for new apps. Subprocess and worker_threads blocking stay. fs writes are allowed only under the OS temp dir, as on Terrapin, where temp-file libraries (tempy, temp-dir) write; it isn't cleaned up per run, and os.tmpdir() is left unpatched: - Every other write is refused with code EROFS, including through a ../ escape, a `<link>/..` path (resolved with the kernel's semantics), a symlink, a rename/link across the boundary, cp, an fd opened outside it, or on the temp root itself, except a mkdir that only makes sure it exists. - unlink, rm, rmdir, rename and the link destination are judged by where the entry itself lives, not where a final symlink points. - Symlink creation is refused, so the temp dir never holds a link this run made that redirects a write elsewhere. - Write-mode fs.open/openSync/fs.promises.open outside the temp dir is refused before it truncates. fds and FileHandles opened inside it stay writable, even by a stream still writing after the run returns, and fds are forgotten on close (including a FileHandle's), so a reused fd number never inherits write access. Path writes from a run that ended are refused. - A FileHandle's own modifying methods (chmod, write, truncate...) only work inside a run on a handle opened for writing there. - Writes to fds 1 and 2 pass through, so console output works. - Wrappers keep Node's promisify metadata, and a reassignment through a clone of a guarded accessor (graceful-fs's patched copy of fs) is recorded instead of throwing. The guard no longer patches anything on import: every bundler that loads the plugin imports it (the build folds it into the main chunk), so installGuards() runs explicitly before each local execution and on a Vite dev server that can execute locally (authenticated, not dev-verify). Vite's module fetch (resolve, load, transform) for a backend function's dynamic import runs outside the blocked scope, through the SSR environment's fetchModule or, on Vite 5, the plugin container, so a project plugin can write while transforming; evaluating the imported module stays blocked. If another plugin release already guards the built-ins, local execution refuses to run rather than run unguarded. trustedFetch, runAllowed and the epoch bookkeeping behind them are removed: with network allowed, they protected nothing the dev server's own credentials in process.env don't already expose. The Jest configurability carve-out and its ts-node tests are gone too, and the shared-context registry moves from `net` to `fs`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7fc5b67 to
8d67510
Compare
…ustom-credentials-file' into tiffany.trinh/apps-2792-network-guard-and-actions-hardening # Conflicts: # packages/plugins/apps/src/vite/local-execution.test.ts
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…d on a cold install Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2792-network-guard-and-actions-hardening # Conflicts: # packages/plugins/apps/src/vite/dev-server.integration.test.ts # packages/plugins/apps/src/vite/index.test.ts
… /tmp readFile, readFileSync and fs.promises.readFile open a path without the guarded fs.open, so a write flag could truncate or create a file outside the temp dir. /tmp is now writable alongside os.tmpdir() outside Windows, matching Terrapin, where os.tmpdir() is /tmp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
|


Motivation
net,fetch,dgram,dns,WebSocket,EventSource), matching Terrapin v2./tmp..env, fail withEROFS.$.Actionsonly checked a call's own top-levelconnectionId.inputscarried anallowedConnectionIdsfield reached the destination with a self-declared scope nothing validated.process.enva backend function's dependencies can read.Architecture
runBlockedscopes the guards to the run's own async chain, so concurrent dev-server work outside the run isn't blocked.Changes
21 changes across network-guard.ts, local-execution.ts, index.ts, auth.ts, execution-epoch.ts, core request helpers, and tests
/tmp(outside Windows, where Terrapin'sos.tmpdir()is/tmp), both captured at install, except amkdirthat names a root; every other write fails withEROFSnetwork-guard.ts,network-guard.test.tsfs.realpathSync.nativeon the raw path, so<link>/..follows the link the way the kernel does instead of collapsing textuallynetwork-guard.ts,network-guard.test.tsunlink,rm,rmdir,renameand a link destination are judged by where the entry itself lives, whilewriteFile,open,truncate,chmodand thecopyFiledestination follow a final symlinknetwork-guard.ts,network-guard.test.tssymlinkandcpare refused everywhere, andmkdtempis checked against the directory it creates rather than its prefixnetwork-guard.ts,network-guard.test.tsnetwork-guard.ts,network-guard.test.tsnetwork-guard.ts,network-guard.test.tsfs.open/openSync/fs.promises.openoutside the temp dir are refused before truncating, and fds and FileHandles opened inside it are recorded per run and revoked onfs.closeor the handle's ownclosenetwork-guard.ts,network-guard.test.tsreadFile/readFileSync/fs.promises.readFilewith a writeflagon a path outside the temp dir are refused, since they open the file without the guardedfs.opennetwork-guard.ts,network-guard.test.tsfs.promises.openis guarded as a plain data property that each install re-wraps, so a reassignment can't drop it andjest.spyOnstill worksnetwork-guard.ts,network-guard.test.tschmod,chown,utimes,truncate,write*,appendFile) only work inside a run on a handle it opened for writingnetwork-guard.ts,network-guard.test.tsnetwork-guard.ts,network-guard.test.tsos.tmpdir()is left unpatched and the temp dir isn't cleaned up per run, so a temp-file library first loaded inside a run keeps workingnetwork-guard.ts,network-guard.test.ts,local-execution.test.tsnetwork-guard.ts,network-guard.test.tsinstallGuards(), called fromconfigureServerwhen local execution can run (auth configured, not dev-verify) and before each local executionindex.ts,local-execution.ts,network-guard.ts,index.test.tsconfigureServerwarns once when graceful-fs or fs-extra loaded before the guards, since their copies offsskip themindex.ts,network-guard.ts,index.test.tsfetchModuleon Vite 6 or the plugin container'sresolveId/load/transformon Vite 5, rejecting a non-string idindex.ts,network-guard.ts,dev-server.integration.test.ts,index.test.tsnetwork-guard.ts,local-execution.ts,local-execution.test.tstrustedFetch,runAllowed, the epoch helpers behind them and the Jest configurability carve-outnetwork-guard.ts,auth.ts,execution-epoch.tsfetchpresent at dev server start, kept across restarts, so a dependency that replaces the global (MSW, instrumentation) doesn't see or break themauth.ts,auth.test.ts,packages/core/src/helpers/request.test.tscloseBundletests observefs/promises.mkdtempthrough a module mock, since the guards make it non-configurable forjest.spyOnin a shared workersrc/index.test.ts$.Actionsrejects a call whoseinputsdeclare their ownallowedConnectionIdson both entry points, its dispatch runs inside the blocked scope, and a threat note plus theauth.tscomment record the residual gaps and accepted credential risklocal-execution.ts,local-execution.test.ts,network-guard.ts,auth.tsQA Instructions
packages/published/vite-plugin/dist/src/index.mjson its own leavesfs,fs.promises.open,os.tmpdir,child_processand the FileHandle prototype unpatched. ✅ VERIFIEDManual QA — real dev server against staging (dd.datad0g.com)
vite.config.tsimports this branch's built plugin (packages/published/vite-plugin/dist/src/index.mjs, rebuilt at 1731214), launched with staging credentials:POST /__dd/executeActionwith{"functionName": "<sha256('src/<file>')>.<fn>", "args": []}, and runs through the real priming flow (Calling Datadog API: .../preview-async→Long-poll response, done: true→Executing "..." in-process).node_modulesdependencies, since the static banned-imports check rejectsfs,osandpathin a.backend.tsfile's own source.fake-fs-dep.tryWriteFileisfs.writeFileSync(target, contents), andfake-fs-dep.tryReadWithFlagisfs.readFileSync(target, { flag, encoding: 'utf8' }).fetch, then a Vite restart:configLoadernotice and the git plugin's "No .git directory found".Blast Radius
fetchdirectly./tmp, such as to the project or the home dir, now fails locally withEROFS.os.tmpdir()keep working.$.Actionsscope rejection, which only refuse what production also refuses.fsandchild_processaccessors are non-configurable for the process lifetime, sojest.spyOnon them fails in a Jest worker that has runinstallGuards();fs.promises.openstays spy-able.Out of Scope / Follow-ups
23 items deferred
DD_API_KEY/DD_APP_KEY/OAuth token stays inprocess.env, readable by a backend function's dependenciesauth.ts)@datadog/apps-cliwould have to pass the credential over another channel (stdin, a socket, an fd), a cross-repo change'error'listener crashes the dev serverclose()leaves its fd recorded in that runFinalizationRegistryif it mattersinstallGuards()calllstat/realpathsyscallsfs.open/openSyncandfs.promises.openfs.promises.opendoesfs/child_process--permissionso Node itself enforces the fs, child-process and worker limitsrunBlocked, unguarded--permissiondirection abovenode:sqlite) andfsfunctions copied before install skip the guards--permissiondirection above$.Actionsdoesn't inject the function's allowlist intoinputs.allowedConnectionIdsthe way the cloud path does, so a meta-action call with no key may reachpreview-asyncunrestrictedallowedConnectionIdskey ininputsis rejected, not one nested deepersymlinkandcpare refused even under the temp dir, where production allows themfs.promises.openwas replaced before install and returned a non-FileHandleopenbefore installprocess.nextTick, so they don't fire under Jest fake timersnextTickFunction.prototypecould make a target look already guardedDocumentation
🤖 Generated with Claude Code