From cf69907015b659e5025efb735ee31bd05c4ee3d5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Feb 2026 03:30:39 +0100 Subject: [PATCH] fix(security): redact Telegram bot tokens in errors --- CHANGELOG.md | 1 + src/infra/errors.ts | 31 ++++++++++++++++++------------- src/logging/redact.test.ts | 10 ++++++++++ src/logging/redact.ts | 2 ++ 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80951a59ac..3e326d61aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code. - Discord: preserve channel session continuity when runtime payloads omit `message.channelId` by falling back to event/raw `channel_id` values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as `sessionKey=unknown`. (#17622) Thanks @shakkernerd. - Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras. +- Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent. - Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx. - Dev tooling: harden git `pre-commit` hook against option injection from malicious filenames (for example `--force`), preventing accidental staging of ignored files. Thanks @mrthankyou. - Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model. diff --git a/src/infra/errors.ts b/src/infra/errors.ts index 1ea7950c2b..e64881d1d6 100644 --- a/src/infra/errors.ts +++ b/src/infra/errors.ts @@ -1,3 +1,5 @@ +import { redactSensitiveText } from "../logging/redact.js"; + export function extractErrorCode(err: unknown): string | undefined { if (!err || typeof err !== "object") { return undefined; @@ -27,20 +29,22 @@ export function hasErrnoCode(err: unknown, code: string): boolean { } export function formatErrorMessage(err: unknown): string { + let formatted: string; if (err instanceof Error) { - return err.message || err.name || "Error"; - } - if (typeof err === "string") { - return err; - } - if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") { - return String(err); - } - try { - return JSON.stringify(err); - } catch { - return Object.prototype.toString.call(err); + formatted = err.message || err.name || "Error"; + } else if (typeof err === "string") { + formatted = err; + } else if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") { + formatted = String(err); + } else { + try { + formatted = JSON.stringify(err); + } catch { + formatted = Object.prototype.toString.call(err); + } } + // Security: best-effort token redaction before returning/logging. + return redactSensitiveText(formatted); } export function formatUncaughtError(err: unknown): string { @@ -48,7 +52,8 @@ export function formatUncaughtError(err: unknown): string { return formatErrorMessage(err); } if (err instanceof Error) { - return err.stack ?? err.message ?? err.name; + const stack = err.stack ?? err.message ?? err.name; + return redactSensitiveText(stack); } return formatErrorMessage(err); } diff --git a/src/logging/redact.test.ts b/src/logging/redact.test.ts index 3e8b754dd7..131c41c6b2 100644 --- a/src/logging/redact.test.ts +++ b/src/logging/redact.test.ts @@ -49,6 +49,16 @@ describe("redactSensitiveText", () => { expect(output).toBe("123456…cdef"); }); + it("masks Telegram Bot API URL tokens", () => { + const input = + "GET https://api.telegram.org/bot123456:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef/getMe HTTP/1.1"; + const output = redactSensitiveText(input, { + mode: "tools", + patterns: defaults, + }); + expect(output).toBe("GET https://api.telegram.org/bot123456…cdef/getMe HTTP/1.1"); + }); + it("redacts short tokens fully", () => { const input = "TOKEN=shortvalue"; const output = redactSensitiveText(input, { diff --git a/src/logging/redact.ts b/src/logging/redact.ts index f79bed7e07..e60766cba4 100644 --- a/src/logging/redact.ts +++ b/src/logging/redact.ts @@ -32,6 +32,8 @@ const DEFAULT_REDACT_PATTERNS: string[] = [ String.raw`\b(AIza[0-9A-Za-z\-_]{20,})\b`, String.raw`\b(pplx-[A-Za-z0-9_-]{10,})\b`, String.raw`\b(npm_[A-Za-z0-9]{10,})\b`, + // Telegram Bot API URLs embed the token as `/bot/...` (no word-boundary before digits). + String.raw`\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b`, String.raw`\b(\d{6,}:[A-Za-z0-9_-]{20,})\b`, ];