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:
@@ -237,17 +237,25 @@ 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}`,
|
||||||
``,
|
``
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isVerbose) {
|
||||||
|
// Full content logging only in debug/trace mode
|
||||||
|
logLines.push(
|
||||||
`=== Input ===`,
|
`=== Input ===`,
|
||||||
JSON.stringify(input, null, 2),
|
JSON.stringify(input, null, 2),
|
||||||
``,
|
``,
|
||||||
@@ -262,9 +270,31 @@ export async function runContainerAgent(
|
|||||||
``,
|
``,
|
||||||
`=== Stdout ===`,
|
`=== Stdout ===`,
|
||||||
stdout
|
stdout
|
||||||
].join('\n');
|
);
|
||||||
fs.writeFileSync(logFile, logContent);
|
} else {
|
||||||
logger.debug({ logFile }, 'Container log written');
|
// 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({
|
||||||
|
|||||||
15
src/db.ts
15
src/db.ts
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/index.ts
28
src/index.ts
@@ -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) {
|
||||||
|
const task = getTask(data.taskId);
|
||||||
|
if (task && (data.isMain || task.group_folder === data.groupFolder)) {
|
||||||
updateTask(data.taskId, { status: 'paused' });
|
updateTask(data.taskId, { status: 'paused' });
|
||||||
logger.info({ taskId: data.taskId }, 'Task paused via IPC');
|
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) {
|
||||||
|
const task = getTask(data.taskId);
|
||||||
|
if (task && (data.isMain || task.group_folder === data.groupFolder)) {
|
||||||
updateTask(data.taskId, { status: 'active' });
|
updateTask(data.taskId, { status: 'active' });
|
||||||
logger.info({ taskId: data.taskId }, 'Task resumed via IPC');
|
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) {
|
||||||
|
const task = getTask(data.taskId);
|
||||||
|
if (task && (data.isMain || task.group_folder === data.groupFolder)) {
|
||||||
deleteTask(data.taskId);
|
deleteTask(data.taskId);
|
||||||
logger.info({ taskId: data.taskId }, 'Task cancelled via IPC');
|
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,8 +360,17 @@ 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;
|
||||||
|
|
||||||
|
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);
|
storeMessage(msg, chatJid, msg.key.fromMe || false, msg.pushName || undefined);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user