Skip to content

Commit f30db18

Browse files
fix(security): Set secure file permissions (0600) for config files
- Add secure-fs.ts utility with writeFileSecure and ensureSecureDir - Config files now created with 0600 (user read/write only) - Directories created with 0700 (user only) - Updated sms-notify.ts, sms-action-runner.ts, sms-webhook.ts, auto-background.ts Prevents other local users from reading sensitive config data.
1 parent 9007bcd commit f30db18

File tree

5 files changed

+65
-27
lines changed

5 files changed

+65
-27
lines changed

src/hooks/auto-background.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* Automatically backgrounds long-running or specific commands
44
*/
55

6-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
6+
import { existsSync, readFileSync } from 'fs';
77
import { join } from 'path';
88
import { homedir } from 'os';
9+
import { writeFileSecure, ensureSecureDir } from './secure-fs.js';
910

1011
export interface AutoBackgroundConfig {
1112
enabled: boolean;
@@ -101,11 +102,8 @@ export function loadConfig(): AutoBackgroundConfig {
101102

102103
export function saveConfig(config: AutoBackgroundConfig): void {
103104
try {
104-
const dir = join(homedir(), '.stackmemory');
105-
if (!existsSync(dir)) {
106-
mkdirSync(dir, { recursive: true });
107-
}
108-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
105+
ensureSecureDir(join(homedir(), '.stackmemory'));
106+
writeFileSecure(CONFIG_PATH, JSON.stringify(config, null, 2));
109107
} catch {
110108
// Silently fail
111109
}

src/hooks/secure-fs.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Secure file system utilities for hooks
3+
* Ensures config files have restricted permissions (0600)
4+
*/
5+
6+
import { writeFileSync, mkdirSync, chmodSync, existsSync } from 'fs';
7+
import { dirname } from 'path';
8+
9+
/**
10+
* Write file with secure permissions (0600 - user read/write only)
11+
* Also ensures parent directory has 0700 permissions
12+
*/
13+
export function writeFileSecure(filePath: string, data: string): void {
14+
const dir = dirname(filePath);
15+
16+
// Create directory with secure permissions if needed
17+
if (!existsSync(dir)) {
18+
mkdirSync(dir, { recursive: true, mode: 0o700 });
19+
}
20+
21+
// Write file
22+
writeFileSync(filePath, data);
23+
24+
// Set secure permissions (user read/write only)
25+
chmodSync(filePath, 0o600);
26+
}
27+
28+
/**
29+
* Ensure directory exists with secure permissions (0700)
30+
*/
31+
export function ensureSecureDir(dirPath: string): void {
32+
if (!existsSync(dirPath)) {
33+
mkdirSync(dirPath, { recursive: true, mode: 0o700 });
34+
} else {
35+
// Set permissions on existing directory
36+
try {
37+
chmodSync(dirPath, 0o700);
38+
} catch {
39+
// Ignore if we can't change permissions (not owner)
40+
}
41+
}
42+
}

src/hooks/sms-action-runner.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
* Security: Uses allowlist-based action execution to prevent command injection
66
*/
77

8-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8+
import { existsSync, readFileSync } from 'fs';
99
import { join } from 'path';
1010
import { homedir } from 'os';
1111
import { execSync, execFileSync } from 'child_process';
1212
import { randomBytes } from 'crypto';
13+
import { writeFileSecure, ensureSecureDir } from './secure-fs.js';
1314

1415
// Allowlist of safe action patterns
1516
const SAFE_ACTION_PATTERNS: Array<{
@@ -77,11 +78,8 @@ export function loadActionQueue(): ActionQueue {
7778

7879
export function saveActionQueue(queue: ActionQueue): void {
7980
try {
80-
const dir = join(homedir(), '.stackmemory');
81-
if (!existsSync(dir)) {
82-
mkdirSync(dir, { recursive: true });
83-
}
84-
writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2));
81+
ensureSecureDir(join(homedir(), '.stackmemory'));
82+
writeFileSecure(QUEUE_PATH, JSON.stringify(queue, null, 2));
8583
} catch {
8684
// Silently fail
8785
}

src/hooks/sms-notify.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
* Optional feature - requires Twilio setup
77
*/
88

9-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9+
import { existsSync, readFileSync } from 'fs';
1010
import { join } from 'path';
1111
import { homedir } from 'os';
1212
import { config as loadDotenv } from 'dotenv';
13+
import { writeFileSecure, ensureSecureDir } from './secure-fs.js';
1314

1415
export type MessageChannel = 'whatsapp' | 'sms';
1516

@@ -232,15 +233,12 @@ function applyEnvVars(config: SMSConfig): void {
232233

233234
export function saveSMSConfig(config: SMSConfig): void {
234235
try {
235-
const dir = join(homedir(), '.stackmemory');
236-
if (!existsSync(dir)) {
237-
mkdirSync(dir, { recursive: true });
238-
}
236+
ensureSecureDir(join(homedir(), '.stackmemory'));
239237
// Don't save sensitive credentials to file
240238
const safeConfig = { ...config };
241239
delete safeConfig.accountSid;
242240
delete safeConfig.authToken;
243-
writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));
241+
writeFileSecure(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));
244242
} catch {
245243
// Silently fail
246244
}

src/hooks/sms-webhook.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212

1313
import { createServer, IncomingMessage, ServerResponse } from 'http';
1414
import { parse as parseUrl } from 'url';
15-
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';
15+
import { existsSync, readFileSync } from 'fs';
1616
import { join } from 'path';
1717
import { homedir } from 'os';
1818
import { createHmac } from 'crypto';
1919
import { execFileSync } from 'child_process';
2020
import { processIncomingResponse, loadSMSConfig } from './sms-notify.js';
2121
import { queueAction, executeActionSafe } from './sms-action-runner.js';
22+
import { writeFileSecure, ensureSecureDir } from './secure-fs.js';
2223

2324
// Security constants
2425
const MAX_BODY_SIZE = 50 * 1024; // 50KB max body
@@ -96,12 +97,13 @@ function storeLatestResponse(
9697
response: string,
9798
action?: string
9899
): void {
99-
const dir = join(homedir(), '.stackmemory');
100-
if (!existsSync(dir)) {
101-
mkdirSync(dir, { recursive: true });
102-
}
103-
const responsePath = join(dir, 'sms-latest-response.json');
104-
writeFileSync(
100+
ensureSecureDir(join(homedir(), '.stackmemory'));
101+
const responsePath = join(
102+
homedir(),
103+
'.stackmemory',
104+
'sms-latest-response.json'
105+
);
106+
writeFileSecure(
105107
responsePath,
106108
JSON.stringify({
107109
promptId,
@@ -215,7 +217,7 @@ function triggerResponseNotification(response: string): void {
215217
// Write signal file for other processes
216218
try {
217219
const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');
218-
writeFileSync(
220+
writeFileSecure(
219221
signalPath,
220222
JSON.stringify({
221223
type: 'sms_response',
@@ -366,7 +368,7 @@ export function startWebhookServer(port: number = 3456): void {
366368
? JSON.parse(readFileSync(statusPath, 'utf8'))
367369
: {};
368370
statuses[payload['MessageSid']] = payload['MessageStatus'];
369-
writeFileSync(statusPath, JSON.stringify(statuses, null, 2));
371+
writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));
370372

371373
res.writeHead(200, { 'Content-Type': 'text/plain' });
372374
res.end('OK');

0 commit comments

Comments
 (0)