From 732c624e6b5ee5a191b05ee02781e689cbf62eba Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sun, 1 Feb 2026 15:51:53 +0200 Subject: [PATCH] Fix security issues: IPC auth, message logging, container logs - Add authorization checks to IPC task operations (pause/resume/cancel) to prevent cross-group task manipulation - Only store message content for registered groups; unregistered chats only get metadata stored for group discovery - Container logs now only include full input/output in debug mode; default logging omits sensitive message content Co-Authored-By: Claude Opus 4.5 --- src/container-runner.ts | 70 +++++++++++++++++++++++++++++------------ src/db.ts | 15 +++++++-- src/index.ts | 42 +++++++++++++++++++------ 3 files changed, 96 insertions(+), 31 deletions(-) diff --git a/src/container-runner.ts b/src/container-runner.ts index 8f13f5b..ee4a684 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -237,34 +237,64 @@ export async function runContainerAgent( clearTimeout(timeout); const duration = Date.now() - startTime; - // Always write stderr to log file for debugging + // Write container log file const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const logFile = path.join(logsDir, `container-${timestamp}.log`); - const logContent = [ + const isVerbose = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'trace'; + + // Build log content - only include full input/output in verbose mode + const logLines = [ `=== Container Run Log ===`, `Timestamp: ${new Date().toISOString()}`, `Group: ${group.name}`, `IsMain: ${input.isMain}`, `Duration: ${duration}ms`, `Exit Code: ${code}`, - ``, - `=== Input ===`, - JSON.stringify(input, null, 2), - ``, - `=== Container Args ===`, - containerArgs.join(' '), - ``, - `=== Mounts ===`, - mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), - ``, - `=== Stderr ===`, - stderr, - ``, - `=== Stdout ===`, - stdout - ].join('\n'); - fs.writeFileSync(logFile, logContent); - logger.debug({ logFile }, 'Container log written'); + `` + ]; + + if (isVerbose) { + // Full content logging only in debug/trace mode + logLines.push( + `=== Input ===`, + JSON.stringify(input, null, 2), + ``, + `=== Container Args ===`, + containerArgs.join(' '), + ``, + `=== Mounts ===`, + mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), + ``, + `=== Stderr ===`, + stderr, + ``, + `=== Stdout ===`, + stdout + ); + } else { + // Minimal logging by default - no message content + logLines.push( + `=== Input Summary ===`, + `Prompt length: ${input.prompt.length} chars`, + `Session ID: ${input.sessionId || 'new'}`, + ``, + `=== Mounts ===`, + mounts.map(m => `${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), + `` + ); + + // Only include stderr/stdout if there was an error + if (code !== 0) { + logLines.push( + `=== Stderr (last 500 chars) ===`, + stderr.slice(-500), + `` + ); + } + } + + fs.writeFileSync(logFile, logLines.join('\n')); + logger.debug({ logFile, verbose: isVerbose }, 'Container log written'); if (code !== 0) { logger.error({ diff --git a/src/db.ts b/src/db.ts index 294e534..855054c 100644 --- a/src/db.ts +++ b/src/db.ts @@ -66,6 +66,19 @@ export function initDatabase(): void { } catch { /* column already exists */ } } +/** + * Store chat metadata only (no message content). + * Used for all chats to enable group discovery without storing sensitive content. + */ +export function storeChatMetadata(chatJid: string, timestamp: string): void { + db.prepare(`INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)`) + .run(chatJid, chatJid, timestamp); +} + +/** + * Store a message with full content. + * Only call this for registered groups where message history is needed. + */ export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFromMe: boolean, pushName?: string): void { if (!msg.key) return; @@ -81,8 +94,6 @@ export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFrom const senderName = pushName || sender.split('@')[0]; const msgId = msg.key.id || ''; - db.prepare(`INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)`) - .run(chatJid, chatJid, timestamp); db.prepare(`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?, ?)`) .run(msgId, chatJid, sender, senderName, content, timestamp, isFromMe ? 1 : 0); } diff --git a/src/index.ts b/src/index.ts index 8212bda..e37cf0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import { IPC_POLL_INTERVAL } from './config.js'; import { RegisteredGroup, Session, NewMessage } from './types.js'; -import { initDatabase, storeMessage, getNewMessages, getMessagesSince, getAllTasks } from './db.js'; +import { initDatabase, storeMessage, storeChatMetadata, getNewMessages, getMessagesSince, getAllTasks, getTaskById } from './db.js'; import { startSchedulerLoop } from './scheduler.js'; import { runContainerAgent, writeTasksSnapshot } from './container-runner.js'; @@ -233,7 +233,7 @@ async function processTaskIpc(data: { isMain?: boolean; }): Promise { // Import db functions dynamically to avoid circular deps - const { createTask, updateTask, deleteTask } = await import('./db.js'); + const { createTask, updateTask, deleteTask, getTaskById: getTask } = await import('./db.js'); const { CronExpressionParser } = await import('cron-parser'); switch (data.type) { @@ -271,22 +271,37 @@ async function processTaskIpc(data: { case 'pause_task': if (data.taskId) { - updateTask(data.taskId, { status: 'paused' }); - logger.info({ taskId: data.taskId }, 'Task paused via IPC'); + const task = getTask(data.taskId); + if (task && (data.isMain || task.group_folder === data.groupFolder)) { + updateTask(data.taskId, { status: 'paused' }); + logger.info({ taskId: data.taskId }, 'Task paused via IPC'); + } else { + logger.warn({ taskId: data.taskId, groupFolder: data.groupFolder }, 'Unauthorized task pause attempt'); + } } break; case 'resume_task': if (data.taskId) { - updateTask(data.taskId, { status: 'active' }); - logger.info({ taskId: data.taskId }, 'Task resumed via IPC'); + const task = getTask(data.taskId); + if (task && (data.isMain || task.group_folder === data.groupFolder)) { + updateTask(data.taskId, { status: 'active' }); + logger.info({ taskId: data.taskId }, 'Task resumed via IPC'); + } else { + logger.warn({ taskId: data.taskId, groupFolder: data.groupFolder }, 'Unauthorized task resume attempt'); + } } break; case 'cancel_task': if (data.taskId) { - deleteTask(data.taskId); - logger.info({ taskId: data.taskId }, 'Task cancelled via IPC'); + const task = getTask(data.taskId); + if (task && (data.isMain || task.group_folder === data.groupFolder)) { + deleteTask(data.taskId); + logger.info({ taskId: data.taskId }, 'Task cancelled via IPC'); + } else { + logger.warn({ taskId: data.taskId, groupFolder: data.groupFolder }, 'Unauthorized task cancel attempt'); + } } break; @@ -345,7 +360,16 @@ async function connectWhatsApp(): Promise { if (!msg.message) continue; const chatJid = msg.key.remoteJid; if (!chatJid || chatJid === 'status@broadcast') continue; - storeMessage(msg, chatJid, msg.key.fromMe || false, msg.pushName || undefined); + + const timestamp = new Date(Number(msg.messageTimestamp) * 1000).toISOString(); + + // Always store chat metadata for group discovery + storeChatMetadata(chatJid, timestamp); + + // Only store full message content for registered groups + if (registeredGroups[chatJid]) { + storeMessage(msg, chatJid, msg.key.fromMe || false, msg.pushName || undefined); + } } }); }