fix: harden logging, memory sync, config merge, and input validation

- Redact sensitive text in file log transport before writing to disk
- Validate Nextcloud Talk room tokens and message IDs against path traversal
- Propagate real errors from ensureDir instead of silently swallowing
- Use Promise.allSettled in memory sync so one bad file does not abort indexing
- Block __proto__/constructor/prototype keys in deepMerge (config includes + voice-call TTS)
This commit is contained in:
issuemakerable 2026-01-29 19:51:20 +09:00
parent 19823c5498
commit d5be6aa3f4
6 changed files with 21 additions and 5 deletions

View File

@ -48,6 +48,9 @@ function normalizeRoomToken(to: string): string {
}
if (!normalized) throw new Error("Room token is required for Nextcloud Talk sends");
if (!/^[a-zA-Z0-9_-]+$/.test(normalized)) {
throw new Error(`Invalid room token: contains disallowed characters`);
}
return normalized;
}
@ -177,6 +180,9 @@ export async function sendReactionNextcloudTalk(
account,
);
const normalizedToken = normalizeRoomToken(roomToken);
if (!/^[a-zA-Z0-9_-]+$/.test(messageId)) {
throw new Error("Invalid message ID: contains disallowed characters");
}
const body = JSON.stringify({ reaction });
const { random, signature } = generateNextcloudTalkSignature({

View File

@ -80,6 +80,7 @@ function deepMerge<T>(base: T, override: T): T {
const result: Record<string, unknown> = { ...base };
for (const [key, value] of Object.entries(override)) {
if (value === undefined) continue;
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
const existing = (base as Record<string, unknown>)[key];
if (isPlainObject(existing) && isPlainObject(value)) {
result[key] = deepMerge(existing, value);

View File

@ -70,6 +70,7 @@ export function deepMerge(target: unknown, source: unknown): unknown {
if (isPlainObject(target) && isPlainObject(source)) {
const result: Record<string, unknown> = { ...target };
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
result[key] = key in result ? deepMerge(result[key], source[key]) : source[key];
}
return result;

View File

@ -8,6 +8,7 @@ import type { MoltbotConfig } from "../config/types.js";
import type { ConsoleStyle } from "./console.js";
import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js";
import { readLoggingConfig } from "./config.js";
import { redactSensitiveText } from "./redact.js";
import { loggingState } from "./state.js";
// Pin to /tmp so mac Debug UI and docs match; os.tmpdir() can be a per-user
@ -96,7 +97,8 @@ function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
logger.attachTransport((logObj: LogObj) => {
try {
const time = logObj.date?.toISOString?.() ?? new Date().toISOString();
const line = JSON.stringify({ ...logObj, time });
const raw = JSON.stringify({ ...logObj, time });
const line = redactSensitiveText(raw);
fs.appendFileSync(settings.file, `${line}\n`, { encoding: "utf8" });
} catch {
// never block on logging failures

View File

@ -19,9 +19,7 @@ export type MemoryChunk = {
};
export function ensureDir(dir: string): string {
try {
fsSync.mkdirSync(dir, { recursive: true });
} catch {}
fsSync.mkdirSync(dir, { recursive: true });
return dir;
}

View File

@ -976,9 +976,17 @@ export class MemoryIndexManager {
progress?: MemorySyncProgressState;
}) {
const files = await listMemoryFiles(this.workspaceDir);
const fileEntries = await Promise.all(
const settled = await Promise.allSettled(
files.map(async (file) => buildFileEntry(file, this.workspaceDir)),
);
const fileEntries = settled.flatMap((result, i) => {
if (result.status === "fulfilled") return [result.value];
log.warn("memory sync: skipping unreadable file", {
file: files[i],
error: String(result.reason),
});
return [];
});
log.debug("memory sync: indexing memory files", {
files: fileEntries.length,
needsFullReindex: params.needsFullReindex,