mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 08:54:04 +01:00
* 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>
72 lines
3.4 KiB
TypeScript
72 lines
3.4 KiB
TypeScript
/**
|
|
* GSTA-56: each way gstack starts Bun keeps a hostile working directory's
|
|
* .env and bunfig.toml out, on every platform the free lanes run, including
|
|
* windows-latest under Git Bash. There MSYS rewrites --config=/dev/null to
|
|
* --config=nul for the native bun.exe, which Bun 1.4.2 reads as an empty
|
|
* config; a native Windows spawn has no /dev/null, so TypeScript callers use
|
|
* bunIsolationArgs() (lib/bun-isolation.ts) instead of spelling the flag.
|
|
*/
|
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { bunIsolationArgs } from '../lib/bun-isolation';
|
|
import { bashCommandInvocation } from '../lib/script-invocation';
|
|
|
|
const PRINT = 'console.log("env=" + (process.env.GSTACK_LAUNCH_PROBE ?? "unset"))';
|
|
let project = '';
|
|
let preloadMarker = '';
|
|
|
|
function bash(command: string, extraEnv: Record<string, string> = {}) {
|
|
const inv = bashCommandInvocation(command);
|
|
if (!inv.ok) throw new Error(inv.error);
|
|
return spawnSync(inv.cmd, inv.argv, { cwd: project, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...extraEnv } });
|
|
}
|
|
|
|
function expectIsolated(r: { status: number | null; stdout: string; stderr: string }, expected = 'env=unset') {
|
|
expect(r.stderr.trim()).toBe('');
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout.trim()).toBe(expected);
|
|
expect(fs.existsSync(preloadMarker)).toBe(false);
|
|
}
|
|
|
|
beforeAll(() => {
|
|
project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-')));
|
|
preloadMarker = path.join(project, 'preload-ran');
|
|
fs.writeFileSync(path.join(project, '.env'), 'GSTACK_LAUNCH_PROBE=from-dotenv\n');
|
|
fs.writeFileSync(path.join(project, 'preload.ts'), `require('fs').writeFileSync(${JSON.stringify(preloadMarker)}, 'ran');\n`);
|
|
fs.writeFileSync(path.join(project, 'bunfig.toml'), 'preload = ["./preload.ts"]\n');
|
|
fs.writeFileSync(path.join(project, 'probe.ts'), PRINT + '\n');
|
|
fs.writeFileSync(path.join(project, 'shebang-probe'), `#!/usr/bin/env -S bun --no-env-file --config=/dev/null\n${PRINT}\n`);
|
|
fs.chmodSync(path.join(project, 'shebang-probe'), 0o755);
|
|
});
|
|
|
|
afterAll(() => { if (project) fs.rmSync(project, { recursive: true, force: true }); });
|
|
|
|
describe('GSTA-56: isolated Bun launches on this platform', () => {
|
|
test('control: a bare bun in the fixture loads its .env and runs its preload', () => {
|
|
const r = spawnSync(process.execPath, ['probe.ts'], { cwd: project, encoding: 'utf8', timeout: 60_000 });
|
|
expect(r.stdout).toContain('env=from-dotenv');
|
|
expect(fs.existsSync(preloadMarker)).toBe(true);
|
|
fs.rmSync(preloadMarker);
|
|
});
|
|
|
|
test('a script with the isolated shebang, executed from bash', () => {
|
|
expectIsolated(bash('./shebang-probe'));
|
|
});
|
|
|
|
test('a bash caller spelling bun --no-env-file --config=/dev/null', () => {
|
|
expectIsolated(bash(`bun --no-env-file --config=/dev/null -e '${PRINT}'`));
|
|
expectIsolated(bash('bun --no-env-file --config=/dev/null run probe.ts'));
|
|
});
|
|
|
|
test('a TypeScript spawn with bunIsolationArgs()', () => {
|
|
expectIsolated(spawnSync(process.execPath, [...bunIsolationArgs(), 'probe.ts'], { cwd: project, encoding: 'utf8', timeout: 60_000 }));
|
|
});
|
|
|
|
test("negative control: the user's shell environment still reaches an isolated launch", () => {
|
|
expectIsolated(bash('./shebang-probe', { GSTACK_LAUNCH_PROBE: 'from-shell' }), 'env=from-shell');
|
|
});
|
|
});
|