Skip to content

[APPS-2792] Harden network-guard, drop local network blocking - #524

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 6 commits into
masterfrom
tiffany.trinh/apps-2792-network-guard-and-actions-hardening
Oct 2, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 6 commits into
masterfrom
tiffany.trinh/apps-2792-network-guard-and-actions-hardening

Conversation

@tyffical

@tyffical tyffical commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

  • Stacked on #523.
  • Local dev allows real network access (net, fetch, dgram, dns, WebSocket, EventSource), matching Terrapin v2.
  • Filesystem writes from a backend function are limited to the OS temp dir and, outside Windows, /tmp.
    • That's the local stand-in for the writable per-run pod Terrapin gives a backend function.
    • Writes anywhere else, such as the project, the home dir or .env, fail with EROFS.
  • Subprocesses and worker threads stay blocked.
  • $.Actions only checked a call's own top-level connectionId.
    • A platform meta-action whose own inputs carried an allowedConnectionIds field reached the destination with a self-declared scope nothing validated.
    • It's now rejected.
  • The dev server's own Datadog credential stays in the same process.env a backend function's dependencies can read.
    • No local dev tool avoids this without process separation.
    • It's recorded as an accepted risk rather than patched.

Architecture

 configureServer                       loadCustomerModuleEntry /
 (auth configured, not dev-verify)     each local execution
            │                                   │
            └────────────────────┬──────────────┘
                                 ▼
 ┌─────────────────────────────────────────────────────────────────┐
 │ installGuards()  patches fs, fs.promises, FileHandle methods,   │
 │                  child_process and worker_threads once, and     │
 │                  records the real temp dir; nothing on import   │
 └─────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
 ┌───────────────────────────────────────────┐       ┌────────────────────────────────────┐
 │ runBlocked(fn)  AsyncLocalStorage scope   │       │ Vite module fetch for fn's dynamic │
 │  • fs writes: only inside the OS temp dir │  ◄─── │ import runs outside the scope:     │
 │  • fds / FileHandles recorded per run,    │       │ SSR fetchModule (Vite 6) or        │
 │    revoked on close                       │       │ pluginContainer resolveId/load/    │
 │  • subprocess / Worker refused            │       │ transform (Vite 5)                 │
 └───────────────────────────────────────────┘       └────────────────────────────────────┘
  • runBlocked scopes the guards to the run's own async chain, so concurrent dev-server work outside the run isn't blocked.
  • Evaluating a dynamically imported module stays inside the scope; only Vite's resolve, load and transform run outside it.

Changes

21 changes across network-guard.ts, local-execution.ts, index.ts, auth.ts, execution-epoch.ts, core request helpers, and tests
What changed File
Writes inside a run are allowed only strictly under the real OS temp dir or /tmp (outside Windows, where Terrapin's os.tmpdir() is /tmp), both captured at install, except a mkdir that names a root; every other write fails with EROFS network-guard.ts, network-guard.test.ts
Paths resolve with fs.realpathSync.native on the raw path, so <link>/.. follows the link the way the kernel does instead of collapsing textually network-guard.ts, network-guard.test.ts
unlink, rm, rmdir, rename and a link destination are judged by where the entry itself lives, while writeFile, open, truncate, chmod and the copyFile destination follow a final symlink network-guard.ts, network-guard.test.ts
symlink and cp are refused everywhere, and mkdtemp is checked against the directory it creates rather than its prefix network-guard.ts, network-guard.test.ts
A Buffer path that doesn't survive a UTF-8 round trip is refused, since its decoded form can name a different file network-guard.ts, network-guard.test.ts
The first run after install waits for the async FileHandle prototype guard and refuses to run if it couldn't install; later runs start synchronously network-guard.ts, network-guard.test.ts
Write-mode fs.open/openSync/fs.promises.open outside the temp dir are refused before truncating, and fds and FileHandles opened inside it are recorded per run and revoked on fs.close or the handle's own close network-guard.ts, network-guard.test.ts
readFile/readFileSync/fs.promises.readFile with a write flag on a path outside the temp dir are refused, since they open the file without the guarded fs.open network-guard.ts, network-guard.test.ts
fs.promises.open is guarded as a plain data property that each install re-wraps, so a reassignment can't drop it and jest.spyOn still works network-guard.ts, network-guard.test.ts
A FileHandle's modifying methods (chmod, chown, utimes, truncate, write*, appendFile) only work inside a run on a handle it opened for writing network-guard.ts, network-guard.test.ts
After a run ends its path writes are refused, while writes on its own fds and handles continue so a fire-and-forget stream finishes network-guard.ts, network-guard.test.ts
os.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 working network-guard.ts, network-guard.test.ts, local-execution.test.ts
Writes to fds 1 and 2 pass through, wrappers keep Node's promisify metadata, and a reassignment through graceful-fs's cloned accessor is recorded instead of throwing network-guard.ts, network-guard.test.ts
Guards install only through installGuards(), called from configureServer when local execution can run (auth configured, not dev-verify) and before each local execution index.ts, local-execution.ts, network-guard.ts, index.test.ts
configureServer warns once when graceful-fs or fs-extra loaded before the guards, since their copies of fs skip them index.ts, network-guard.ts, index.test.ts
Vite's module fetch for a dynamic import runs outside the blocked scope through the SSR environment's fetchModule on Vite 6 or the plugin container's resolveId/load/transform on Vite 5, rejecting a non-string id index.ts, network-guard.ts, dev-server.integration.test.ts, index.test.ts
Local execution refuses to run, before evaluating the customer module, when another plugin release's guards are already installed network-guard.ts, local-execution.ts, local-execution.test.ts
Removes all network blocking along with trustedFetch, runAllowed, the epoch helpers behind them and the Jest configurability carve-out network-guard.ts, auth.ts, execution-epoch.ts
The dev server's authenticated requests use the fetch present at dev server start, kept across restarts, so a dependency that replaces the global (MSW, instrumentation) doesn't see or break them auth.ts, auth.test.ts, packages/core/src/helpers/request.test.ts
The apps closeBundle tests observe fs/promises.mkdtemp through a module mock, since the guards make it non-configurable for jest.spyOn in a shared worker src/index.test.ts
$.Actions rejects a call whose inputs declare their own allowedConnectionIds on both entry points, its dispatch runs inside the blocked scope, and a threat note plus the auth.ts comment record the residual gaps and accepted credential risk local-execution.ts, local-execution.test.ts, network-guard.ts, auth.ts

QA Instructions

yarn typecheck:all
# Exit 0. ✅ VERIFIED

yarn workspace @dd/tests test:unit --testPathPatterns "plugins/apps"
# Test Suites: 32 passed, 32 total. Tests: 714 passed, 714 total. ✅ VERIFIED

yarn format && yarn lint
# Both exit clean, git status --short shows no changes. ✅ VERIFIED

yarn cli integrity
# Exit 0, git status --short shows no changes. ✅ VERIFIED

# The guard patches real core modules for the whole Jest worker, so the full suite is the check
# that no other package's tests collide with it.
yarn build:all && yarn test:unit
# Test Suites: 96 passed, 96 total. Tests: 1 skipped, 2502 passed, 2503 total. ✅ VERIFIED
  • Test URL: CI checks on this PR
  • Importing packages/published/vite-plugin/dist/src/index.mjs on its own leaves fs, fs.promises.open, os.tmpdir, child_process and the FileHandle prototype unpatched. ✅ VERIFIED
  • No feature flag is involved.
Manual QA — real dev server against staging (dd.datad0g.com)
  • Scaffolded app whose vite.config.ts imports this branch's built plugin (packages/published/vite-plugin/dist/src/index.mjs, rebuilt at 1731214), launched with staging credentials:
cd qa-app && DEMO_API_KEY=local-demo-123 dd-auth --domain dd.datad0g.com -- npx vite --port 5178 --strictPort
  • Each function is called through the real endpoint, POST /__dd/executeAction with {"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).
  • The fs calls live in fake node_modules dependencies, since the static banned-imports check rejects fs, os and path in a .backend.ts file's own source. fake-fs-dep.tryWriteFile is fs.writeFileSync(target, contents), and fake-fs-dep.tryReadWithFlag is fs.readFileSync(target, { flag, encoding: 'utf8' }).
// node_modules/fake-tmp-dep/index.js
const fs = require('fs');
const os = require('os');
const path = require('path');

function tmpPath(name) {
    return path.join(os.tmpdir(), name);
}

function readText(target) {
    return fs.readFileSync(target, 'utf8');
}

module.exports = { tmpPath, readText };
// src/qaWritePolicy.backend.ts
import { tryReadWithFlag, tryWriteFile } from 'fake-fs-dep';
import { readText, tmpPath } from 'fake-tmp-dep';

const PROJECT_DIR_TARGET = '/Users/tiffany.trinh/qa-app-2792-verify/qa-524-should-not-exist.txt';

export async function writeUnderTmpdir() {
    const target = tmpPath(`qa-524-${Date.now()}`);
    const contents = `qa-524 ${new Date().toISOString()}`;
    try {
        tryWriteFile(target, contents);
    } catch (err) {
        return { target, written: false, error: err instanceof Error ? err.message : String(err) };
    }
    const readBack = readText(target);
    return { target, written: true, readBackMatches: readBack === contents, readBack };
}

export async function writeUnderProjectDir() {
    try {
        tryWriteFile(PROJECT_DIR_TARGET, 'this should never be written');
        return { target: PROJECT_DIR_TARGET, written: true };
    } catch (err) {
        return { target: PROJECT_DIR_TARGET, written: false, error: err instanceof Error ? err.message : String(err) };
    }
}

const READ_FLAG_TARGET = '/Users/tiffany.trinh/qa-app-2792-verify/qa-524-readfile-target.txt';

export async function readFileWithWriteFlag() {
    try {
        tryReadWithFlag(READ_FLAG_TARGET, 'w');
        return { target: READ_FLAG_TARGET, opened: true };
    } catch (err) {
        return { target: READ_FLAG_TARGET, opened: false, error: err instanceof Error ? err.message : String(err) };
    }
}
# networkProbe: fetch, fs write to /tmp (outside os.tmpdir() on macOS), execSync through fake-net-dep
# {"fetch":"allowed (status 200)","fsWrite":"allowed (/tmp)","subprocess":"blocked: Spawning a subprocess is not allowed in backend functions."} ✅ VERIFIED (2026-10-02 20:54:00 UTC)

# qaWritePolicy.writeUnderTmpdir
# {"target":"/var/folders/.../T/qa-524-1790974441559","written":true,"readBackMatches":true,...} ✅ VERIFIED (2026-10-02 20:54:01 UTC)

# qaWritePolicy.writeUnderProjectDir
# {"target":".../qa-app-2792-verify/qa-524-should-not-exist.txt","written":false,"error":"Writing to the filesystem is not allowed in backend functions outside os.tmpdir() or /tmp."}
# ls on that path afterwards: No such file or directory ✅ VERIFIED (2026-10-02 20:54:02 UTC)

# qaWritePolicy.readFileWithWriteFlag (target pre-created with "keep me")
# {"target":".../qa-app-2792-verify/qa-524-readfile-target.txt","opened":false,"error":"Writing to the filesystem is not allowed in backend functions outside os.tmpdir() or /tmp."}
# cat on that path afterwards: keep me ✅ VERIFIED (2026-10-02 20:54:02 UTC)

# scopedAction.tryScopedAction
# {"rejected":true,"message":"Action $.Actions.test.action must not declare its own allowedConnectionIds in inputs — this function's own allowlist already governs which connections it can use."} ✅ VERIFIED (2026-10-02 20:54:03 UTC)

# getMonitorSummary (action-catalog listMonitors against staging)
# {"monitorsReturned":100,"byState":[...]} ✅ VERIFIED (2026-10-02 20:54:14 UTC)

# authorizedDigest (apps-backend getInitiatingUser + listMonitors + searchIncidents)
# {"authorized":true,...,"activeIncidents":25} ✅ VERIFIED (2026-10-02 20:54:17 UTC)
  • A dependency that replaces the global fetch, then a Vite restart:
// node_modules/fake-fetch-dep/index.js
function replaceGlobalFetch() {
    globalThis.fetch = () => Promise.reject(new Error('replaced fetch was called'));
    return { replaced: true };
}

module.exports = { replaceGlobalFetch };
// src/fetchReplace.backend.ts
import { replaceGlobalFetch } from 'fake-fetch-dep';

export async function replaceFetch() {
    return replaceGlobalFetch();
}
# fetchReplace.replaceFetch → {"replaced":true}, then getMonitorSummary
# {"monitorsReturned":100,...} ✅ VERIFIED (2026-10-02 20:54:19 UTC)

touch vite.config.ts
# [vite] vite.config.ts changed, restarting server... / server restarted.
# getMonitorSummary {"monitorsReturned":100,...} and authorizedDigest {"authorized":true,...} ✅ VERIFIED (2026-10-02 20:54:26 UTC)
  • The startup log shows no crash and no graceful-fs warning.
    • The only other warnings are Vite's configLoader notice and the git plugin's "No .git directory found".

Blast Radius

  • Local dev only: production builds and the cloud execution path don't load the guards.
  • No feature flag.
  • Medium for network: a dependency reachable from a backend function can make arbitrary outbound requests during local development.
    • This matches Terrapin v2.
    • The static banned-globals check still keeps a backend function's own source from calling fetch directly.
  • Medium for filesystem writes: a backend function that writes outside the OS temp dir and /tmp, such as to the project or the home dir, now fails locally with EROFS.
    • Temp-file libraries that write under os.tmpdir() keep working.
  • Low for subprocess and worker blocking and the $.Actions scope rejection, which only refuse what production also refuses.
  • Guarded fs and child_process accessors are non-configurable for the process lifetime, so jest.spyOn on them fails in a Jest worker that has run installGuards(); fs.promises.open stays spy-able.

Out of Scope / Follow-ups

23 items deferred
Item Status Next step
The dev server's own DD_API_KEY/DD_APP_KEY/OAuth token stays in process.env, readable by a backend function's dependencies Accepted risk (see auth.ts) @datadog/apps-cli would have to pass the credential over another channel (stdin, a socket, an fd), a cross-repo change
A refused write on a stream with no 'error' listener crashes the dev server Documented residual gap Revisit if a real dependency hits it
Writes on an fd or handle opened outside the current run (module top-level code, an earlier run) are refused even under the temp dir Documented residual gap Revisit if a real dependency hits it
A FileHandle closed by garbage collection rather than close() leaves its fd recorded in that run Deferred Revoke through a FinalizationRegistry if it matters
If capturing the real temp dir fails at install, later installs don't retry it, so every write is refused Deferred Retry the capture on each installGuards() call
A malformed file URL makes the callback and promise forms throw synchronously instead of reporting through the callback or promise Deferred Catch it and report through the method's own error channel
Every guarded write inside a run costs lstat/realpath syscalls Deferred Measure before optimizing
The write-mode open admission is duplicated between fs.open/openSync and fs.promises.open Deferred Share one admission helper
FileHandle prototype guards are applied once, so a later replacement of a prototype method isn't re-wrapped Deferred Re-check on each install, as fs.promises.open does
In-process monkeypatching only reaches code that uses Node's JS fs/child_process Proposed direction Run backend functions in a child Node process with --permission so Node itself enforces the fs, child-process and worker limits
If this release's guards install first, an older release loaded later skips its own install and runs unguarded Accepted Only fixable in the older release; install a single version
A customer module's top-level code runs before runBlocked, unguarded Documented Covered by the --permission direction above
Listening on a Unix socket or named pipe creates a file outside the temp dir Documented residual gap Allowed along with network
Native modules with their own I/O (e.g. node:sqlite) and fs functions copied before install skip the guards Documented; graceful-fs and fs-extra get a startup warning Covered by the --permission direction above
Files written under the temp dir aren't cleaned up per run Accepted, matches plain Node Revisit if leftovers become a problem
Locally, $.Actions doesn't inject the function's allowlist into inputs.allowedConnectionIds the way the cloud path does, so a meta-action call with no key may reach preview-async unrestricted Needs investigation Confirm how the server treats a missing key, then inject the allowlist locally if it's unrestricted
Only a top-level allowedConnectionIds key in inputs is rejected, not one nested deeper Deferred; no known action takes a nested one Walk nested inputs if one appears
symlink and cp are refused even under the temp dir, where production allows them Deferred; a false local failure, not a boundary gap Allow both when every path stays under the temp dir
On Vite 5, a dynamic import of a linked package outside the root can create a file watcher inside the run's scope, so later HMR writes fail until restart Deferred; Vite 5 monorepos only Exempt the watcher setup like the plugin container
The FileHandle prototype is marked guarded even if fs.promises.open was replaced before install and returned a non-FileHandle Deferred; needs a replaced open before install Mark it only after a method was patched
A run that ends holding a writable fd stays in the scope registry Deferred Prune closed scopes on fd close
Callback and spawn-error paths use the live process.nextTick, so they don't fire under Jest fake timers Deferred; tests only Use the captured nextTick
The install marker is read through the prototype chain, so a polluted Function.prototype could make a target look already guarded Deferred; needs code that runs before install Use an own-property check

Documentation

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T18:24:06.885221Z 8d67510 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-network-guard-and-actions-hardening branch 5 times, most recently from bf884e8 to af21eb5 Compare September 29, 2026 14:38
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Live Debugger Runtime Benchmark

SDK-loaded dormant-probe runtime overhead, measured against an uninstrumented bundle in the same browser session.

Browser Workload Quality Per-call overhead upper
chrome Hot clean <= 3.35 ns
chrome Tiny clean <= 0.28 ns
firefox Hot clean <= 7.76 ns
firefox Tiny clean <= 0.27 ns
safari Hot clean <= 4.22 ns
safari Tiny clean <= 0.64 ns

What do the Tiny and Hot workloads represent?

Browser Debugger SDK: 7.15.0 · built 2026-09-29 · S3 ETag cc0c53c4

Full diagnostics
browser  workload  quality  per-call overhead upper  overhead upper         95% CI        A/A diag       block CI  acf(1)   baseline  instrumented                         samples
-------  --------  -------  -----------------------  --------------  -------------  --------------  -------------  ------  ---------  ------------  ------------------------------
chrome   Hot       clean                 <= 3.35 ns        <= 7.76%  3.34..3.35 ns  -0.01..0.00 ns  3.34..3.35 ns   -0.09  51.400 ms     55.375 ms   102 (trim 20%, outliers 4.9%)
chrome   Tiny      clean                 <= 0.28 ns        <= 4.38%  0.27..0.28 ns  -0.01..0.01 ns  0.27..0.28 ns    0.06  54.483 ms     56.795 ms  102 (trim 20%, outliers 13.7%)
firefox  Hot       clean                 <= 7.76 ns       <= 36.45%  7.75..7.76 ns  -0.01..0.00 ns  7.75..7.76 ns    0.20  40.600 ms     55.400 ms   102 (trim 20%, outliers 6.9%)
firefox  Tiny      clean                 <= 0.27 ns        <= 2.27%  0.27..0.27 ns  -0.00..0.00 ns  0.27..0.27 ns   -0.01  50.800 ms     51.940 ms   102 (trim 20%, outliers 5.9%)
safari   Hot       clean                 <= 4.22 ns       <= 20.04%  4.22..4.22 ns  -0.00..0.00 ns  4.22..4.22 ns   -0.04  46.160 ms     55.400 ms   102 (trim 20%, outliers 6.9%)
safari   Tiny      clean                 <= 0.64 ns       <= 19.71%  0.56..0.63 ns  -0.00..0.00 ns  0.54..0.64 ns    0.13  48.660 ms     57.660 ms   102 (trim 20%, outliers 1.0%)

Raw samples are in the live-debugger-runtime-bench-results artifact.

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-network-guard-and-actions-hardening branch from af21eb5 to 7a9975a Compare September 29, 2026 14:56
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-network-guard-and-actions-hardening branch 2 times, most recently from a6e26b1 to c413714 Compare September 29, 2026 18:31
@tyffical tyffical changed the title [APPS-2792] Harden network-guard fs writes, $.Actions scoping, and Jest detection [APPS-2792] Harden network-guard fs writes/subprocess/$.Actions scoping, drop local network blocking Sep 29, 2026
@tyffical tyffical changed the title [APPS-2792] Harden network-guard fs writes/subprocess/$.Actions scoping, drop local network blocking [APPS-2792] Harden network-guard, drop local network blocking Sep 29, 2026
@tyffical
tyffical requested a balanced review from Copilot September 30, 2026 18:01

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-network-guard-and-actions-hardening branch 2 times, most recently from b35e776 to 5f94ba9 Compare September 30, 2026 23:07
@tyffical
tyffical added this pull request to stack #534 October 1, 2026 03:12
@tyffical
tyffical marked this pull request as ready for review October 1, 2026 03:13
@tyffical
tyffical requested a review from a team as a code owner October 1, 2026 03:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Customer module initialization remains outside the filesystem guard, permitting top-level dependency writes.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

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`. */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@tyffical
tyffical requested a review from ksun154 October 1, 2026 15:06
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-network-guard-and-actions-hardening branch from 5f94ba9 to 7fc5b67 Compare October 1, 2026 15:14
tyffical and others added 2 commits October 1, 2026 13:53
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>
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-network-guard-and-actions-hardening branch from 7fc5b67 to 8d67510 Compare October 1, 2026 18:02
@tyffical
tyffical requested a balanced review from Copilot October 1, 2026 18:16
…ustom-credentials-file' into tiffany.trinh/apps-2792-network-guard-and-actions-hardening

# Conflicts:
#	packages/plugins/apps/src/vite/local-execution.test.ts
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 8d67510a88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Two critical filesystem-guard bypasses remain: lossy Buffer-path decoding and incomplete FileHandle initialization before execution.

Review effort: Balanced
Findings: 3 High severity

Open (3)

Comment thread packages/plugins/apps/src/vite/network-guard.ts
Comment thread packages/plugins/apps/src/vite/network-guard.ts
…d on a cold install

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from tiffany.trinh/apps-2792-revert-custom-credentials-file to master October 1, 2026 21:52
…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
Comment thread packages/plugins/apps/src/vite/network-guard.ts Outdated
Comment thread packages/plugins/apps/src/vite/network-guard.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>
@tyffical

tyffical commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-10-02 21:32:56 UTC ℹ️ Start processing command /merge


2026-10-02 21:33:00 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 2m (p90).


2026-10-02 21:34:24 UTC ℹ️ MergeQueue: This merge request was merged

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 4b245ca into master Oct 2, 2026
9 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the tiffany.trinh/apps-2792-network-guard-and-actions-hardening branch October 2, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants