fix(security): sanitize QQBot debug log values

Sanitizes QQBot debug log values to remediate CodeQL alert 230.
This commit is contained in:
Vincent Koc
2026-04-30 00:37:05 -07:00
committed by GitHub
parent 13e917e292
commit 2d748e4ac1
3 changed files with 61 additions and 3 deletions

View File

@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
- Security/outbound: strip re-formed HTML tags during plain-text sanitization so nested tag fragments cannot leave a CodeQL-detected `<script>` sequence behind. Thanks @vincentkoc.
- Security/secrets: compare credential bytes with padded timing-safe buffers instead of hashing candidate passwords before equality checks. Thanks @vincentkoc.
- Security/QQBot: sanitize debug log arguments before writing to `console.*`, so gateway payload fields cannot forge extra log lines when debug logging is enabled. Thanks @vincentkoc.
- CLI/agents/status: keep `openclaw agents`, text `agents list`, and plain text `status` on read-only metadata paths so human output no longer preloads plugin runtimes or live channel scans before printing. Fixes #74195. Thanks @NianJiuZst.
- Agents/local models: derive context-window guard thresholds from the effective model window with 4k/8k safety floors, so small local models are no longer rejected by fixed 16k/32k preflight cutoffs. Fixes #42999. Thanks @chengjialu8888.
- Media: treat legacy Word/OLE attachments with `application/msword` or `application/x-cfb` MIME as binary so printable-looking `.doc` files are not embedded into prompts as text. Fixes #54176; carries forward #54380. Thanks @andyliu.

View File

@@ -0,0 +1,28 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { debugLog, sanitizeDebugLogValue } from "./log.js";
const originalDebug = process.env.QQBOT_DEBUG;
afterEach(() => {
if (originalDebug === undefined) {
delete process.env.QQBOT_DEBUG;
} else {
process.env.QQBOT_DEBUG = originalDebug;
}
vi.restoreAllMocks();
});
describe("QQBot debug logging", () => {
it("neutralizes control characters in log values", () => {
expect(sanitizeDebugLogValue("before\nforged\r\tentry")).toBe("before forged entry");
});
it("sanitizes arguments before debug console output", () => {
process.env.QQBOT_DEBUG = "1";
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
debugLog("prefix", "line one\nline two");
expect(logSpy).toHaveBeenCalledWith("prefix", "line one line two");
});
});

View File

@@ -9,24 +9,53 @@
*/
const isDebug = () => !!process.env.QQBOT_DEBUG;
const MAX_LOG_VALUE_CHARS = 4096;
export function sanitizeDebugLogValue(value: unknown): string {
let text: string;
if (typeof value === "string") {
text = value;
} else if (value instanceof Error) {
text = value.stack || value.message;
} else {
try {
text = JSON.stringify(value) ?? String(value);
} catch {
text = String(value);
}
}
const sanitized = text
.replace(/\p{Cc}/gu, " ")
.replace(/\s+/g, " ")
.trim();
if (sanitized.length <= MAX_LOG_VALUE_CHARS) {
return sanitized;
}
return `${sanitized.slice(0, MAX_LOG_VALUE_CHARS)}...`;
}
function sanitizeDebugLogArgs(args: unknown[]): string[] {
return args.map(sanitizeDebugLogValue);
}
/** Debug-level log; only outputs when QQBOT_DEBUG is enabled. */
export function debugLog(...args: unknown[]): void {
if (isDebug()) {
console.log(...args);
console.log(...sanitizeDebugLogArgs(args));
}
}
/** Debug-level warning; only outputs when QQBOT_DEBUG is enabled. */
export function debugWarn(...args: unknown[]): void {
if (isDebug()) {
console.warn(...args);
console.warn(...sanitizeDebugLogArgs(args));
}
}
/** Debug-level error; only outputs when QQBOT_DEBUG is enabled. */
export function debugError(...args: unknown[]): void {
if (isDebug()) {
console.error(...args);
console.error(...sanitizeDebugLogArgs(args));
}
}