mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-15 01:15:29 +02:00
fix(browse): self-repair broken Windows DACLs on state dirs (#1605)
icacls '/inheritance:r /grant:r' can partially fail on localized or domain accounts: inheritance strips but the user grant doesn't resolve, leaving a machine-SID-only DACL the owner can't even list — the sidebar/PTY failure chain in #1605, caused by the very hardening call meant to protect the dir. mkdirSecure now verifies listability after hardening (a real readdir — fs.accessSync doesn't consult NTFS ACLs) and repairs via icacls /reset, re-hardens, and if hardening breaks access again leaves inherited ACLs: functional-but-unhardened beats hardened-but-unusable. The icacls calls carry windowsHide (#1835's last two sites) and the fork's static spawn-hide tripwire lands here, pinning every covered site. file-permissions.test.ts is already in the windows-free-tests curated shard, so the DACL contract executes on windows-latest. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Sina Matian
Claude Fable 5
parent
270f1a038a
commit
fa4e4c3f2c
@@ -22,6 +22,7 @@ import {
|
||||
writeSecureFile,
|
||||
appendSecureFile,
|
||||
mkdirSecure,
|
||||
repairBrokenDacl,
|
||||
__resetWarnedForTests,
|
||||
} from '../src/file-permissions';
|
||||
|
||||
@@ -145,4 +146,35 @@ describe('mkdirSecure', () => {
|
||||
expect(fs.existsSync(path.join(tmpDir, 'a', 'b'))).toBe(true);
|
||||
expect(fs.existsSync(d)).toBe(true);
|
||||
});
|
||||
|
||||
test('created directory is listable by the creating process', () => {
|
||||
// #1605 contract: whatever ACL hardening happens, the client must be
|
||||
// able to read its own state dir immediately after creation.
|
||||
const d = path.join(tmpDir, 'state');
|
||||
mkdirSecure(d);
|
||||
fs.writeFileSync(path.join(d, 'browse.json'), '{}');
|
||||
expect(fs.readdirSync(d)).toContain('browse.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('repairBrokenDacl', () => {
|
||||
test('is a no-op on non-Windows platforms', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
const d = path.join(tmpDir, 'dir');
|
||||
fs.mkdirSync(d);
|
||||
expect(() => repairBrokenDacl(d)).not.toThrow();
|
||||
});
|
||||
|
||||
test('on Windows, does not throw and directory stays listable', () => {
|
||||
if (process.platform !== 'win32') return;
|
||||
const d = path.join(tmpDir, 'dir');
|
||||
fs.mkdirSync(d);
|
||||
expect(() => repairBrokenDacl(d)).not.toThrow();
|
||||
expect(() => fs.readdirSync(d)).not.toThrow();
|
||||
});
|
||||
|
||||
test('on Windows, swallows icacls failure on a nonexistent path', () => {
|
||||
if (process.platform !== 'win32') return;
|
||||
expect(() => repairBrokenDacl(path.join(tmpDir, 'nonexistent'))).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Static tripwire for #1835: child spawns reachable on Windows must pass
|
||||
* windowsHide, or every daemon relaunch / taskkill / icacls / powershell
|
||||
* invocation flashes a black console window (and can steal focus).
|
||||
*
|
||||
* Source-level, same style as server-auth.test.ts / cdp-session-cleanup.test.ts:
|
||||
* cheap, deterministic, runs on every platform.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const SRC = (f: string) => fs.readFileSync(path.join(import.meta.dir, '../src', f), 'utf-8');
|
||||
|
||||
/** Every occurrence of `needle` in `src` must have `windowsHide` within the
|
||||
* next `window` chars (the spawn's options object). */
|
||||
function expectHideNearEvery(src: string, needle: string, window = 400): void {
|
||||
let idx = src.indexOf(needle);
|
||||
expect(idx).toBeGreaterThanOrEqual(0);
|
||||
while (idx !== -1) {
|
||||
const slice = src.slice(idx, idx + window);
|
||||
expect(slice).toMatch(/windowsHide:\s*true/);
|
||||
idx = src.indexOf(needle, idx + needle.length);
|
||||
}
|
||||
}
|
||||
|
||||
describe('windowsHide on Windows-reachable spawns (#1835)', () => {
|
||||
test('daemon launch paths in cli.ts pass windowsHide', () => {
|
||||
const cli = SRC('cli.ts');
|
||||
// Installed path: node -e launcher — both the outer spawnSync and the
|
||||
// inner detached daemon spawn (inside the launcher code string).
|
||||
expect(cli).toContain('detached:true,windowsHide:true');
|
||||
expectHideNearEvery(cli, "'-e', launcherCode]");
|
||||
// Dev fallback: detached bun spawn.
|
||||
expectHideNearEvery(cli, "nodeSpawn('bun'");
|
||||
// taskkill (killServer).
|
||||
expectHideNearEvery(cli, "'taskkill'");
|
||||
});
|
||||
|
||||
test('Windows-only process probes pass windowsHide', () => {
|
||||
// tasklist in isProcessAlive — runs in polling loops.
|
||||
expectHideNearEvery(SRC('error-handling.ts'), "'tasklist'");
|
||||
// powershell DPAPI + tasklist in cookie import.
|
||||
const cookie = SRC('cookie-import-browser.ts');
|
||||
expectHideNearEvery(cookie, "'powershell'");
|
||||
expectHideNearEvery(cookie, "'tasklist'");
|
||||
});
|
||||
|
||||
test('icacls calls in file-permissions.ts pass windowsHide', () => {
|
||||
const perms = SRC('file-permissions.ts');
|
||||
expect((perms.match(/'icacls'/g) || []).length).toBeGreaterThanOrEqual(3);
|
||||
expectHideNearEvery(perms, "'icacls'");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user