Files
gstack/lib/bun-isolation.ts
T
Garry Tanandcapy-ai[bot] 4c6fa89cdc v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1) (#3312)
* security: isolate every gstack Bun entry point from the audited checkout's .env and bunfig.toml

Bun auto-loads .env, .env.local and .env.<NODE_ENV> from its cwd and runs a
cwd bunfig.toml preload before the program starts. gstack runs inside the repo
it audits, so a committed .env could steer state roots, the claude binary or
spend caps, and a bunfig preload ran arbitrary code in every bin and hook.

Every bun shebang, shell and skill caller, TypeScript spawn and compiled build
now disables both (--no-env-file --config=/dev/null, bunIsolationArgs(),
--no-compile-autoload-dotenv/bunfig), with a ratchet and hostile-fixture
regressions.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: name the directly executed scripts in the isolation ratchet

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: follow the isolation flags in touchfiles, copy lists, recipe assertions and the harness pin

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: keep browse/src/bun-child-config.ts unchanged so the native cookie qualification still binds

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(windows): the empty bunfig outlives its process so the timeline hook's detached repair child can read it

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: plan-mode render pattern accepts the isolation flags

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-20 00:24:28 +00:00

57 lines
2.5 KiB
TypeScript

/**
* How gstack starts Bun (GSTA-56). Bun configures itself from the directory it
* starts in: `.env`, `.env.local` and `.env.<NODE_ENV>` become environment
* variables, and a `bunfig.toml` preload runs code before the entry point.
* gstack runs inside the user's repository, so that directory belongs to the
* code under review. Every Bun gstack starts passes --no-env-file and an empty
* --config: shebangs and shell callers spell it
* `bun --no-env-file --config=/dev/null`, TypeScript spawns use
* bunIsolationArgs(). test/bun-entry-isolation.test.ts holds every entry point
* to that form.
*
* POSIX passes /dev/null. Native Windows has no /dev/null and `--config=NUL`
* panicked Bun 1.3.10 while parsing the flag (#3085), so a TypeScript spawn on
* Windows gets an empty bunfig in a fixed directory under the per-user temp
* root, re-checked to be a zero-byte regular file before each spawn; anything
* else is replaced, so the file cannot become a preload vector. The file is
* never removed: a detached child (the timeline hook's repair) reads it after
* the parent that named it has exited.
* browse/src/bun-child-config.ts keeps its own copy of emptyBunConfig(): the
* Windows native cookie qualification binds that file's bytes and bundles it
* without lib/.
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
function isEmptyRegularFile(file: string): boolean {
try {
const stat = fs.lstatSync(file);
return stat.isFile() && stat.size === 0;
} catch {
return false;
}
}
export function emptyBunConfig(platform: NodeJS.Platform = process.platform): string {
if (platform !== 'win32') return '/dev/null';
const dir = path.join(os.tmpdir(), 'gstack-empty-bunfig');
const file = path.join(dir, 'bunfig.toml');
if (isEmptyRegularFile(file)) return file;
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
if (!fs.lstatSync(dir).isDirectory()) throw new Error(`empty Bun config directory ${dir} is not a directory`);
fs.rmSync(file, { recursive: true, force: true });
try {
fs.closeSync(fs.openSync(file, 'wx', 0o600));
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
}
if (!isEmptyRegularFile(file)) throw new Error(`empty Bun config ${file} is not a zero-byte file`);
return file;
}
/** The flags that keep a spawned Bun from reading the working directory's .env and bunfig.toml. */
export function bunIsolationArgs(platform: NodeJS.Platform = process.platform): string[] {
return ['--no-env-file', `--config=${emptyBunConfig(platform)}`];
}