fix(design): create OpenAI key file owner-only to close write-then-chmod race

saveApiKey wrote ~/.gstack/openai.json at the default umask (typically
0644) and only tightened it to 0600 with a following chmodSync. Between
the write and the chmod the file containing the OpenAI API key is
group/world-readable, so any local user on a shared host can read the key
in that window (CWE-377 insecure file creation / CWE-367 TOCTOU).

Pass { mode: 0o600 } to writeFileSync so the file is created owner-only up
front, matching the convention already used for session files in
design/src/session.ts (#859). The trailing chmodSync is kept as a backstop
to tighten a pre-existing loose file.

Adds a regression test asserting the key file is 0600 (no group/other
bits) even when written under a permissive umask.
This commit is contained in:
Bunlong Heng 2026-08-06 13:59:54 -04:00
parent a3259400a3
commit ab4da32cb9
2 changed files with 25 additions and 1 deletions

View File

@ -111,7 +111,10 @@ export function describeApiKeySource(resolution: ApiKeyResolution): string {
export function saveApiKey(key: string): void {
const dir = path.dirname(configPath());
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(configPath(), JSON.stringify({ api_key: key }, null, 2));
// Create the file owner-only up front so the API key is never briefly
// world/group-readable in the window between write and chmod. The trailing
// chmodSync is kept as a backstop to tighten a pre-existing loose file.
fs.writeFileSync(configPath(), JSON.stringify({ api_key: key }, null, 2), { mode: 0o600 });
fs.chmodSync(configPath(), 0o600);
}

View File

@ -111,6 +111,27 @@ describe("resolveApiKeyInfo", () => {
});
});
describe("saveApiKey", () => {
test("stores the key file owner-only, even under a permissive umask", () => {
// The OpenAI key file must never be group/other-readable. saveApiKey now
// creates it with mode 0600 up front (matching session.ts / #859) instead
// of writing at the default umask and tightening afterwards, so the key is
// not briefly world-readable in the write-then-chmod window (CWE-377/367).
const prevUmask = process.umask(0o000);
try {
saveApiKey("sk-secret-value");
} finally {
process.umask(prevUmask);
}
const keyPath = path.join(tmpHome, ".gstack", "openai.json");
const mode = fs.statSync(keyPath).mode & 0o777;
expect(mode).toBe(0o600);
// No group/other read/write/exec bits.
expect(mode & 0o077).toBe(0);
});
});
describe("requireApiKey", () => {
test("prints source disclosure without leaking the key", () => {
process.env.OPENAI_API_KEY = "sk-secret-value";