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 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-01 15:51:53 +02:00
parent 552b26cc95
commit 732c624e6b
3 changed files with 96 additions and 31 deletions

View File

@@ -237,34 +237,64 @@ export async function runContainerAgent(
clearTimeout(timeout); clearTimeout(timeout);
const duration = Date.now() - startTime; 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 timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const logFile = path.join(logsDir, `container-${timestamp}.log`); 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 ===`, `=== Container Run Log ===`,
`Timestamp: ${new Date().toISOString()}`, `Timestamp: ${new Date().toISOString()}`,
`Group: ${group.name}`, `Group: ${group.name}`,
`IsMain: ${input.isMain}`, `IsMain: ${input.isMain}`,
`Duration: ${duration}ms`, `Duration: ${duration}ms`,
`Exit Code: ${code}`, `Exit Code: ${code}`,
``, ``
`=== Input ===`, ];
JSON.stringify(input, null, 2),
``, if (isVerbose) {
`=== Container Args ===`, // Full content logging only in debug/trace mode
containerArgs.join(' '), logLines.push(
``, `=== Input ===`,
`=== Mounts ===`, JSON.stringify(input, null, 2),
mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), ``,
``, `=== Container Args ===`,
`=== Stderr ===`, containerArgs.join(' '),
stderr, ``,
``, `=== Mounts ===`,
`=== Stdout ===`, mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'),
stdout ``,
].join('\n'); `=== Stderr ===`,
fs.writeFileSync(logFile, logContent); stderr,
logger.debug({ logFile }, 'Container log written'); ``,
`=== 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) { if (code !== 0) {
logger.error({ logger.error({

View File

@@ -66,6 +66,19 @@ export function initDatabase(): void {
} catch { /* column already exists */ } } 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 { export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFromMe: boolean, pushName?: string): void {
if (!msg.key) return; if (!msg.key) return;
@@ -81,8 +94,6 @@ export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFrom
const senderName = pushName || sender.split('@')[0]; const senderName = pushName || sender.split('@')[0];
const msgId = msg.key.id || ''; 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 (?, ?, ?, ?, ?, ?, ?)`) 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); .run(msgId, chatJid, sender, senderName, content, timestamp, isFromMe ? 1 : 0);
} }

View File

@@ -19,7 +19,7 @@ import {
IPC_POLL_INTERVAL IPC_POLL_INTERVAL
} from './config.js'; } from './config.js';
import { RegisteredGroup, Session, NewMessage } from './types.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 { startSchedulerLoop } from './scheduler.js';
import { runContainerAgent, writeTasksSnapshot } from './container-runner.js'; import { runContainerAgent, writeTasksSnapshot } from './container-runner.js';
@@ -233,7 +233,7 @@ async function processTaskIpc(data: {
isMain?: boolean; isMain?: boolean;
}): Promise<void> { }): Promise<void> {
// Import db functions dynamically to avoid circular deps // 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'); const { CronExpressionParser } = await import('cron-parser');
switch (data.type) { switch (data.type) {
@@ -271,22 +271,37 @@ async function processTaskIpc(data: {
case 'pause_task': case 'pause_task':
if (data.taskId) { if (data.taskId) {
updateTask(data.taskId, { status: 'paused' }); const task = getTask(data.taskId);
logger.info({ taskId: data.taskId }, 'Task paused via IPC'); 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; break;
case 'resume_task': case 'resume_task':
if (data.taskId) { if (data.taskId) {
updateTask(data.taskId, { status: 'active' }); const task = getTask(data.taskId);
logger.info({ taskId: data.taskId }, 'Task resumed via IPC'); 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; break;
case 'cancel_task': case 'cancel_task':
if (data.taskId) { if (data.taskId) {
deleteTask(data.taskId); const task = getTask(data.taskId);
logger.info({ taskId: data.taskId }, 'Task cancelled via IPC'); 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; break;
@@ -345,7 +360,16 @@ async function connectWhatsApp(): Promise<void> {
if (!msg.message) continue; if (!msg.message) continue;
const chatJid = msg.key.remoteJid; const chatJid = msg.key.remoteJid;
if (!chatJid || chatJid === 'status@broadcast') continue; 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);
}
} }
}); });
} }