From c7ff45b602c3a1f4a2cee244bdf5fdea1f602a24 Mon Sep 17 00:00:00 2001 From: adevwithpurpose Date: Sat, 15 Aug 2026 19:18:20 -0300 Subject: [PATCH] fix(backend): redact client IPs and account prefixes from default proxy logs (#10348) --- .../fixes/10348-default-logs-redact-client.md | 1 + src/lib/proxyLogger.ts | 52 +++++++++++++++- tests/unit/proxy-10348-log-redaction.test.ts | 60 +++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/10348-default-logs-redact-client.md create mode 100644 tests/unit/proxy-10348-log-redaction.test.ts diff --git a/changelog.d/fixes/10348-default-logs-redact-client.md b/changelog.d/fixes/10348-default-logs-redact-client.md new file mode 100644 index 0000000000..4c3aa0a00f --- /dev/null +++ b/changelog.d/fixes/10348-default-logs-redact-client.md @@ -0,0 +1 @@ +- fix(backend): redact client IPs and account prefixes from default proxy logs (#10348) diff --git a/src/lib/proxyLogger.ts b/src/lib/proxyLogger.ts index 327d1d9b4f..79bb4e5af4 100644 --- a/src/lib/proxyLogger.ts +++ b/src/lib/proxyLogger.ts @@ -105,6 +105,45 @@ function loadFromDb() { loadFromDb(); +// Default-off override that restores the verbose [ProxyEgress] console line (raw +// client/egress IPs + account prefix). Kept OFF by default so the process log leaks +// neither IPs nor the account prefix. Deliberately NOT coupled to debugMode +// (src/lib/db/settings.ts defaults debugMode to true) — this verbosity is opt-in only. +// Storage (in-memory ring buffer + SQLite) is untouched and always keeps full IPs. +const PROXY_LOG_INCLUDE_IPS = + process.env.PROXY_LOG_INCLUDE_IPS === "true" || + process.env.PROXY_LOG_INCLUDE_IPS === "1"; + +/** + * Pure formatter for the [ProxyEgress] process-log line (#10348). At the default level it + * emits a short, IP/prefix-free summary; when details are opted in it restores the full + * verbose line including client/egress IPs and the account. Extracted as a separate + * function so it is unit-testable without patching console.log and so the change never + * grows logProxyEvent itself. + */ +export function formatProxyEgressConsoleLine(params: { + provider: string | null; + account: string | null; + clientIp: string | null; + egressIp: string | null; + level: string; + proxyHost: string | null | undefined; + status: string; + includeDetails?: boolean; +}): string { + const provider = params.provider || "-"; + const status = params.status; + if (!params.includeDetails) { + return `[ProxyEgress] ${provider} status=${status}`; + } + const proxy = params.proxyHost ? `:${params.proxyHost}` : ""; + return ( + `[ProxyEgress] ${provider}/${params.account || "-"} ` + + `in=${params.clientIp || "?"} out=${params.egressIp || "?"} ` + + `proxy=${params.level}${proxy} status=${status}` + ); +} + // ──────────────── Log a proxy event ──────────────── export function logProxyEvent(entry: ProxyLogInput) { @@ -131,9 +170,16 @@ export function logProxyEvent(entry: ProxyLogInput) { // IP each account is entering (clientIp) and leaving (egressIp) by. if (log.proxy || log.egressIp) { console.log( - `[ProxyEgress] ${log.provider || "-"}/${log.account || "-"} ` + - `in=${log.clientIp || "?"} out=${log.egressIp || "?"} ` + - `proxy=${log.level}${log.proxy ? `:${log.proxy.host}` : ""} status=${log.status}` + formatProxyEgressConsoleLine({ + provider: log.provider, + account: log.account, + clientIp: log.clientIp, + egressIp: log.egressIp, + level: log.level, + proxyHost: log.proxy?.host, + status: log.status, + includeDetails: PROXY_LOG_INCLUDE_IPS, + }) ); } diff --git a/tests/unit/proxy-10348-log-redaction.test.ts b/tests/unit/proxy-10348-log-redaction.test.ts new file mode 100644 index 0000000000..615cbc3adf --- /dev/null +++ b/tests/unit/proxy-10348-log-redaction.test.ts @@ -0,0 +1,60 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// Regression guard for #10348 — default process logs must not leak client/egress IPs +// or the raw account prefix. Storage (in-memory ring buffer + SQLite) stays intact; +// only the process-log emission changes. +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-proxy-10348-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const proxyLogger = await import("../../src/lib/proxyLogger.ts"); + +function resetStorage() { + proxyLogger.clearProxyLogs(); + core.closeDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(() => resetStorage()); +test.after(() => resetStorage()); + +test("[10348] default ProxyEgress console line redacts client IP, egress IP, and account prefix", () => { + const captured: string[] = []; + const origConsole = console.log; + console.log = (...args: unknown[]) => { + captured.push(args.map(String).join(" ")); + }; + try { + proxyLogger.logProxyEvent({ + status: "error", + provider: "codex", + clientIp: "198.51.100.7", + egressIp: "203.0.113.9", + account: "aabbccdd", + level: "account", + }); + } finally { + console.log = origConsole; + } + const line = captured.find((l) => l.includes("[ProxyEgress]")); + assert.ok(line, "expected a [ProxyEgress] console line"); + assert.ok(line!.includes("codex"), "expected provider in the line"); + assert.ok(line!.includes("status=error"), "expected status=error in the line"); + assert.ok( + !line!.includes("198.51.100.7"), + "client IP must be redacted from the console line by default" + ); + assert.ok( + !line!.includes("203.0.113.9"), + "egress IP must be redacted from the console line by default" + ); + assert.ok( + !line!.includes("aabbccdd"), + "account prefix must be redacted from the console line by default" + ); +}); \ No newline at end of file