mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(logging): redact Telegram bot tokens from timeout URLs (#99428)
* fix(net-policy): redact Telegram bot tokens from timeout URLs * fix(net-policy): extend Telegram bot token redaction to all hostnames * refactor(net-policy): centralize bot token URL redaction --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
f5c700bc4b
commit
4abdf0f3b5
@@ -45,6 +45,19 @@ describe("redactSensitiveUrl", () => {
|
||||
"https://example.com/mcp?safe=value",
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts Telegram bot tokens from URL paths", () => {
|
||||
expect(
|
||||
redactSensitiveUrl(
|
||||
"https://telegram.internal/bot123456:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcd/getMe",
|
||||
),
|
||||
).toBe("https://telegram.internal/bot***/getMe");
|
||||
expect(
|
||||
redactSensitiveUrl(
|
||||
"https://api.telegram.org/bot123456%3AABCDEFGHIJKLMNOPQRSTUVWXYZ_abcd/getMe",
|
||||
),
|
||||
).toBe("https://api.telegram.org/bot***/getMe");
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactSensitiveUrlLikeString", () => {
|
||||
@@ -89,6 +102,14 @@ describe("redactSensitiveUrlLikeString", () => {
|
||||
),
|
||||
).toBe("wss://***:***@[bad-host/socket?token=***&keep=visible)");
|
||||
});
|
||||
|
||||
it("redacts Telegram bot tokens from URL-like fallback strings", () => {
|
||||
expect(
|
||||
redactSensitiveUrlLikeString(
|
||||
"timeout /bot123456:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcd/sendMessage and keep /bot/settings",
|
||||
),
|
||||
).toBe("timeout /bot***/sendMessage and keep /bot/settings");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSensitiveUrlQueryParamName", () => {
|
||||
|
||||
@@ -41,6 +41,14 @@ const SENSITIVE_URL_QUERY_PARAM_NAMES = new Set([
|
||||
// category Lo, so \p{C}\p{Z} alone would let them splice sensitive key names.
|
||||
const URL_QUERY_NAME_SEPARATOR_RE = /[\p{C}\p{Z}\u115F\u1160\u3164\uFFA0+]/gu;
|
||||
|
||||
// Telegram Bot API credentials live in `/bot<token>/...` path segments rather
|
||||
// than userinfo or query params. Keep this shape aligned with logging/redact.ts.
|
||||
const TELEGRAM_BOT_TOKEN_PATH_RE = /\/bot\d{6,}(?::|%3[aA])[A-Za-z0-9_-]{20,}(?=\/|$)/giu;
|
||||
|
||||
function redactSensitiveUrlPath(value: string): string {
|
||||
return value.replace(TELEGRAM_BOT_TOKEN_PATH_RE, "/bot***");
|
||||
}
|
||||
|
||||
function normalizeUrlQueryParamName(name: string): string {
|
||||
const stripped = name.replace(URL_QUERY_NAME_SEPARATOR_RE, "");
|
||||
try {
|
||||
@@ -82,6 +90,11 @@ export function redactSensitiveUrl(value: string): string {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
let mutated = false;
|
||||
const redactedPath = redactSensitiveUrlPath(parsed.pathname);
|
||||
if (redactedPath !== parsed.pathname) {
|
||||
parsed.pathname = redactedPath;
|
||||
mutated = true;
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
parsed.username = parsed.username ? "***" : "";
|
||||
parsed.password = parsed.password ? "***" : "";
|
||||
@@ -105,9 +118,10 @@ export function redactSensitiveUrlLikeString(value: string): string {
|
||||
if (redactedUrl !== value) {
|
||||
return redactedUrl;
|
||||
}
|
||||
return value
|
||||
const redactedFallback = value
|
||||
.replace(/\/\/([^@/?#\s]+)@/g, "//***:***@")
|
||||
.replace(/([?&])([^=&]+)=([^&]*)/g, (match, prefix: string, key: string) =>
|
||||
isSensitiveUrlQueryParamName(key) ? `${prefix}${key}=***` : match,
|
||||
);
|
||||
return redactSensitiveUrlPath(redactedFallback);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,17 @@ vi.mock("../logging/subsystem.js", () => ({
|
||||
import { buildTimeoutAbortSignal, fetchWithTimeout } from "./fetch-timeout.js";
|
||||
import { MAX_SAFE_TIMEOUT_DELAY_MS } from "./timer-delay.js";
|
||||
|
||||
function captureTimeoutLogUrl(url: string): Promise<Record<string, unknown>> {
|
||||
const { cleanup } = buildTimeoutAbortSignal({ timeoutMs: 25, operation: "unit-test", url });
|
||||
return vi.advanceTimersByTimeAsync(25).then(() => {
|
||||
const record = requireWarnRecord(0);
|
||||
cleanup();
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
||||
const SYNTHETIC_TELEGRAM_BOT_TOKEN = "123456:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcd";
|
||||
|
||||
function requireWarnCall(callIndex: number): [string, Record<string, unknown>] {
|
||||
const call = warn.mock.calls[callIndex];
|
||||
if (!call) {
|
||||
@@ -144,6 +155,52 @@ describe("buildTimeoutAbortSignal", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("redacts Telegram bot tokens from parseable timeout URLs", async () => {
|
||||
const record = await captureTimeoutLogUrl(
|
||||
`https://api.telegram.org/bot${SYNTHETIC_TELEGRAM_BOT_TOKEN}/sendMessage?chat_id=1`,
|
||||
);
|
||||
|
||||
expect(record.url).toBe("https://api.telegram.org/bot***/sendMessage");
|
||||
expect(JSON.stringify(record)).not.toContain(SYNTHETIC_TELEGRAM_BOT_TOKEN);
|
||||
});
|
||||
|
||||
it("redacts Telegram bot tokens from custom self-hosted API root timeout URLs", async () => {
|
||||
const record = await captureTimeoutLogUrl(
|
||||
`https://telegram.internal/bot${SYNTHETIC_TELEGRAM_BOT_TOKEN}/getMe?chat_id=1`,
|
||||
);
|
||||
|
||||
expect(record.url).toBe("https://telegram.internal/bot***/getMe");
|
||||
expect(JSON.stringify(record)).not.toContain(SYNTHETIC_TELEGRAM_BOT_TOKEN);
|
||||
});
|
||||
|
||||
it("redacts Telegram bot tokens from proxy API root timeout URLs", async () => {
|
||||
const record = await captureTimeoutLogUrl(
|
||||
`https://tg-proxy.example.com/bot${SYNTHETIC_TELEGRAM_BOT_TOKEN}/sendMessage?foo=bar`,
|
||||
);
|
||||
|
||||
expect(record.url).toBe("https://tg-proxy.example.com/bot***/sendMessage");
|
||||
expect(JSON.stringify(record)).not.toContain(SYNTHETIC_TELEGRAM_BOT_TOKEN);
|
||||
});
|
||||
|
||||
it("redacts Telegram bot tokens from unparseable (fallback) timeout URL strings", async () => {
|
||||
const record = await captureTimeoutLogUrl(
|
||||
`/bot${SYNTHETIC_TELEGRAM_BOT_TOKEN}/sendMessage?chat_id=1`,
|
||||
);
|
||||
|
||||
expect(record.url).toBe("/bot***/sendMessage");
|
||||
expect(JSON.stringify(record)).not.toContain(SYNTHETIC_TELEGRAM_BOT_TOKEN);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["https://example.com/bot/settings?safe=1", "https://example.com/bot/settings"],
|
||||
["https://example.com/bots/chat?safe=1", "https://example.com/bots/chat"],
|
||||
["https://example.com/robot/test?safe=1", "https://example.com/robot/test"],
|
||||
["/bot123:status?safe=1", "/bot123:status"],
|
||||
["/bot123456:short/status?safe=1", "/bot123456:short/status"],
|
||||
])("keeps ordinary bot-like timeout path %s visible", async (input, expected) => {
|
||||
expect((await captureTimeoutLogUrl(input)).url).toBe(expected);
|
||||
});
|
||||
|
||||
it("tags fetch timeout aborts so callers can distinguish them from parent aborts", async () => {
|
||||
const fetchFn = vi.fn<typeof fetch>(
|
||||
async (_input, init) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Fetch timeout helpers wrap fetch calls with timeout and abort behavior.
|
||||
import { redactSensitiveUrlLikeString } from "@openclaw/net-policy/redact-sensitive-url";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveSafeTimeoutDelayMs } from "./timer-delay.js";
|
||||
|
||||
@@ -39,15 +40,17 @@ function sanitizeTimeoutLogUrl(rawUrl: string | undefined): string | undefined {
|
||||
parsed.password = "";
|
||||
parsed.search = "";
|
||||
parsed.hash = "";
|
||||
const value = parsed.toString();
|
||||
const value = redactSensitiveUrlLikeString(parsed.toString());
|
||||
return value.length > LOG_URL_MAX_CHARS ? `${value.slice(0, LOG_URL_MAX_CHARS)}...` : value;
|
||||
} catch {
|
||||
const withoutQueryOrHash = trimmed.split(URL_SECRET_SUFFIX_PATTERN, 1)[0] ?? "";
|
||||
const cleaned = withoutQueryOrHash
|
||||
.replace(/[\r\n\u2028\u2029]+/g, " ")
|
||||
.replace(/\p{Cc}+/gu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const cleaned = redactSensitiveUrlLikeString(
|
||||
withoutQueryOrHash
|
||||
.replace(/[\r\n\u2028\u2029]+/g, " ")
|
||||
.replace(/\p{Cc}+/gu, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim(),
|
||||
);
|
||||
if (!cleaned) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user