mirror of https://github.com/garrytan/gstack.git
gstack's ~/.gstack/ state directory holds bearer tokens, canary tokens, agent
queue contents (with prompt history), session state, security-decision logs,
and saved cookie bundles — all written with { mode: 0o600 } / 0o700. On Windows,
those mode bits are a silent no-op: Node's fs module doesn't translate POSIX
modes to NTFS ACLs, and inherited ACLs leave every "restricted" file readable
by other principals on the machine (verified via icacls — six ACEs, the
intended user is the LAST of six).
Threat model is non-trivial on:
- Self-hosted CI runners (different service account on the same Windows box
can read developer tokens, canary tokens, prompt history)
- Shared development machines (agencies, studios, lab environments)
- Multi-tenant servers with shared home directories
Orthogonal to v1.24.0.0's binary-resolution work — complementary at the write
side. v1.24's bin/gstack-paths resolves ~/.gstack/ correctly across plugin /
global / local installs; this PR ensures files written into those resolved
paths actually get the POSIX 0o600 semantic translated to NTFS.
The fix:
- New browse/src/file-permissions.ts (158 LOC, 5 public + 1 test-reset).
restrictFilePermissions / restrictDirectoryPermissions wrap chmod (POSIX)
or icacls /inheritance:r /grant:r <user>:(F) (Windows). writeSecureFile /
appendSecureFile / mkdirSecure are drop-in wrappers for the common patterns.
- 19 call sites converted across 9 source files: browser-manager.ts,
browser-skill-write.ts, cli.ts, config.ts, meta-commands.ts,
security-classifier.ts, security.ts (4 sites), server.ts (5 sites),
terminal-agent.ts (8 sites), tunnel-denial-log.ts.
- (OI)(CI) inheritance flags on directories mean files created via fs.write*
*inside* an mkdirSecure-created dir inherit the owner-only ACL automatically
— important for tunnel-denial-log.ts where appends use async fsp.appendFile.
Error handling: icacls failures (nonexistent path, missing icacls.exe, hardened
environments) log a one-shot warning to stderr and proceed. Once-per-process
gating prevents log spam if the condition persists. Filesystem stays
functional; the file just ends up with inherited ACLs.
Test plan:
- bun test browse/test/file-permissions.test.ts — 13 pass, 0 fail (POSIX
mode-bit assertions, Windows no-throw, mkdir idempotence, recursive
creation, Buffer payloads, append-creates-then-reapplies-once semantics)
- bun test browse/test/security.test.ts — 38 pass, 0 fail (existing security
test suite plus the bash-binary resolution tests added in fix #1119; the
converted writeFileSync/appendFileSync/mkdirSync sites in security.ts
integrate cleanly)
- Empirical icacls before/after on a real file — 6 ACEs → 1 ACE
- bun build typecheck on all modified files — clean (server.ts has a
pre-existing playwright-core/electron resolution issue unrelated to this PR)
POSIX behavior is bit-identical to old code — fs.chmodSync(path, 0o6XX) on the
helper's POSIX branch matches the inline { mode: 0o6XX } it replaces. Linux
and macOS see no behavior change.
Inviting pushback on three judgment calls (in PR description):
1. icacls vs npm library
2. ACL scope — just user, or user + SYSTEM?
3. Graceful degradation — once-per-process warn, not silent, not hard-fail.
|
||
|---|---|---|
| .. | ||
| fixtures | ||
| activity.test.ts | ||
| adversarial-security.test.ts | ||
| batch.test.ts | ||
| browse-client.test.ts | ||
| browser-manager-unit.test.ts | ||
| browser-skill-commands.test.ts | ||
| browser-skill-write.test.ts | ||
| browser-skills-e2e.test.ts | ||
| browser-skills-storage.test.ts | ||
| build.test.ts | ||
| bun-polyfill.test.ts | ||
| cdp-allowlist.test.ts | ||
| cdp-e2e.test.ts | ||
| cdp-mutex.test.ts | ||
| claude-bin.test.ts | ||
| commands.test.ts | ||
| compare-board.test.ts | ||
| config.test.ts | ||
| content-security.test.ts | ||
| cookie-import-browser.test.ts | ||
| cookie-picker-routes.test.ts | ||
| data-platform.test.ts | ||
| domain-skills-e2e.test.ts | ||
| domain-skills-storage.test.ts | ||
| dual-listener.test.ts | ||
| dx-polish.test.ts | ||
| error-handling.test.ts | ||
| file-drop.test.ts | ||
| file-permissions.test.ts | ||
| find-browse.test.ts | ||
| findport.test.ts | ||
| from-file-path-validation.test.ts | ||
| gstack-config.test.ts | ||
| gstack-update-check.test.ts | ||
| handoff.test.ts | ||
| learnings-injection.test.ts | ||
| pair-agent-e2e.test.ts | ||
| pair-agent-tunnel-eval.test.ts | ||
| path-validation.test.ts | ||
| pdf-flags.test.ts | ||
| platform.test.ts | ||
| security-adversarial-fixes.test.ts | ||
| security-adversarial.test.ts | ||
| security-audit-r2.test.ts | ||
| security-bench-ensemble-live.test.ts | ||
| security-bench-ensemble.test.ts | ||
| security-bench.test.ts | ||
| security-bunnative.test.ts | ||
| security-classifier.test.ts | ||
| security-integration.test.ts | ||
| security-live-playwright.test.ts | ||
| security-review-flow.test.ts | ||
| security-sidepanel-dom.test.ts | ||
| security-source-contracts.test.ts | ||
| security.test.ts | ||
| server-auth.test.ts | ||
| sidebar-integration.test.ts | ||
| sidebar-security.test.ts | ||
| sidebar-tabs.test.ts | ||
| sidebar-unit.test.ts | ||
| sidebar-ux.test.ts | ||
| skill-token.test.ts | ||
| snapshot.test.ts | ||
| sse-session-cookie.test.ts | ||
| state-ttl.test.ts | ||
| tab-each.test.ts | ||
| tab-isolation.test.ts | ||
| telemetry.test.ts | ||
| terminal-agent-integration.test.ts | ||
| terminal-agent.test.ts | ||
| test-server.ts | ||
| token-registry.test.ts | ||
| tunnel-gate-unit.test.ts | ||
| url-validation.test.ts | ||
| watch.test.ts | ||
| watchdog.test.ts | ||
| welcome-page.test.ts | ||