Compare commits

..

1 Commits

Author SHA1 Message Date
adevwithpurpose
c7ff45b602 fix(backend): redact client IPs and account prefixes from default proxy logs (#10348) 2026-08-15 19:18:20 -03:00
6 changed files with 112 additions and 67 deletions

View File

@@ -0,0 +1 @@
- fix(backend): redact client IPs and account prefixes from default proxy logs (#10348)

View File

@@ -1 +0,0 @@
- fix(providers): GitLab Duo falls back to the public Code Suggestions endpoint when direct_access returns 401 (#10365)

View File

@@ -583,20 +583,10 @@ export class GitlabExecutor extends BaseExecutor {
}
if (response.status === 401) {
if (input.log) {
input.log.warn(
"GITLAB-DUO",
"direct_access exchange rejected (401); falling back to public completions endpoint"
);
}
return {
target: {
mode: "monolith",
url: endpoints.publicCompletionsUrl,
headers: buildMonolithHeaders(credentials.accessToken || null),
},
target: null,
credentials,
errorResponse: null,
errorResponse: toOpenAIError(401, "GitLab Duo direct access token request was rejected"),
};
}

View File

@@ -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,
})
);
}

View File

@@ -261,54 +261,3 @@ test("GitlabExecutor falls back to the public Code Suggestions endpoint when dir
globalThis.fetch = originalFetch;
}
});
// #10365: a 401 from the direct_access exchange must ALSO fall back to the public
// Code Suggestions completions endpoint (same resilience as the 403-disabled case
// above), instead of surfacing an opaque 401 token error with no fallback.
test("GitlabExecutor falls back to the public Code Suggestions endpoint when direct_access returns 401", async () => {
const executor = getExecutor("gitlab-duo") as GitlabExecutor;
const originalFetch = globalThis.fetch;
const calls: string[] = [];
globalThis.fetch = async (url) => {
calls.push(String(url));
if (String(url) === "https://gitlab.example.com/api/v4/code_suggestions/direct_access") {
return jsonResponse({ error: "invalid_token" }, 401);
}
return jsonResponse({
model: { name: "code-gecko" },
choices: [{ text: "monolith fallback works" }],
});
};
try {
const result = await executor.execute({
model: "gitlab-duo-code-suggestions",
body: {
messages: [{ role: "user", content: "Say hello" }],
},
stream: false,
credentials: {
accessToken: "oauth-access",
providerSpecificData: {
baseUrl: "https://gitlab.example.com",
},
},
signal: AbortSignal.timeout(10_000),
log: null,
});
assert.deepEqual(calls, [
"https://gitlab.example.com/api/v4/code_suggestions/direct_access",
"https://gitlab.example.com/api/v4/code_suggestions/completions",
]);
const body = (await result.response.json()) as any;
assert.equal(body.model, "code-gecko");
assert.match(body.choices[0].message.content, /monolith fallback works/i);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -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"
);
});