fix(security): use writeFileSync for credential files — Bun.write ignores mode option (#2742)

Bun.write does not support the `mode` option, so credential config files
(Hetzner, DigitalOcean, AWS, OpenRouter) were created with 0644 permissions
instead of the intended 0600, exposing API tokens to other local users.

Switch to node:fs writeFileSync which correctly applies file permissions.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-03-17 22:09:36 -07:00 committed by GitHub
parent 7fe1bdf6b3
commit f35696434a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 9 additions and 9 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.21.2",
"version": "0.21.3",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -4,7 +4,7 @@ import type { CloudInstance, VMConnection } from "../history.js";
import type { CloudInitTier } from "../shared/agents";
import { createHash, createHmac } from "node:crypto";
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { normalize } from "node:path";
import { getErrorMessage } from "@openrouter/spawn-shared";
import * as v from "valibot";
@ -66,7 +66,7 @@ export async function saveCredsToConfig(accessKeyId: string, secretAccessKey: st
mode: 0o700,
});
const payload = `{\n "accessKeyId": ${jsonEscape(accessKeyId)},\n "secretAccessKey": ${jsonEscape(secretAccessKey)},\n "region": ${jsonEscape(region)}\n}\n`;
await Bun.write(configPath, payload, {
writeFileSync(configPath, payload, {
mode: 0o600,
});
}

View file

@ -3,7 +3,7 @@
import type { CloudInstance, VMConnection } from "../history.js";
import type { CloudInitTier } from "../shared/agents";
import { mkdirSync, readFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { normalize } from "node:path";
import * as p from "@clack/prompts";
import { getErrorMessage, isNumber, isString, toObjectArray, toRecord } from "@openrouter/spawn-shared";
@ -239,7 +239,7 @@ async function saveConfig(values: Record<string, unknown>): Promise<void> {
recursive: true,
mode: 0o700,
});
await Bun.write(configPath, JSON.stringify(values, null, 2) + "\n", {
writeFileSync(configPath, JSON.stringify(values, null, 2) + "\n", {
mode: 0o600,
});
}

View file

@ -3,7 +3,7 @@
import type { CloudInstance, VMConnection } from "../history.js";
import type { CloudInitTier } from "../shared/agents";
import { mkdirSync, readFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { normalize } from "node:path";
import { getErrorMessage, isNumber, isString, toObjectArray, toRecord } from "@openrouter/spawn-shared";
import { handleBillingError, isBillingError, showNonBillingError } from "../shared/billing-guidance";
@ -130,7 +130,7 @@ async function saveTokenToConfig(token: string): Promise<void> {
mode: 0o700,
});
const escaped = jsonEscape(token);
await Bun.write(configPath, `{\n "api_key": ${escaped},\n "token": ${escaped}\n}\n`, {
writeFileSync(configPath, `{\n "api_key": ${escaped},\n "token": ${escaped}\n}\n`, {
mode: 0o600,
});
}

View file

@ -1,6 +1,6 @@
// shared/oauth.ts — OpenRouter OAuth flow + API key management
import { mkdirSync, readFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { getErrorMessage, isString } from "@openrouter/spawn-shared";
import * as v from "valibot";
@ -259,7 +259,7 @@ async function saveOpenRouterKey(key: string): Promise<void> {
recursive: true,
mode: 0o700,
});
await Bun.write(
writeFileSync(
configPath,
JSON.stringify(
{