fix(diagnostics): harden capture redaction and discord metadata fetch (#71303)

This commit is contained in:
Vincent Koc
2026-04-24 17:51:12 -07:00
committed by GitHub
parent 25a02825a5
commit 718dffd2f2
6 changed files with 111 additions and 63 deletions

View File

@@ -1,5 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { finalizeDebugProxyCapture, initializeDebugProxyCapture } from "./runtime.js";
import {
captureHttpExchange,
finalizeDebugProxyCapture,
initializeDebugProxyCapture,
} from "./runtime.js";
const storeState = vi.hoisted(() => {
const events: Record<string, unknown>[] = [];
@@ -82,4 +86,42 @@ describe("debug proxy runtime", () => {
expect(events.some((event) => event.kind === "request")).toBe(true);
expect(events.some((event) => event.kind === "response")).toBe(true);
});
it("redacts sensitive request and response headers before persistence", async () => {
initializeDebugProxyCapture("test");
captureHttpExchange({
url: "https://discord.com/api/v10/gateway/bot",
method: "GET",
requestHeaders: {
Authorization: "Bot discord-token",
Cookie: "sid=session-token",
"x-api-key": "provider-key",
"content-type": "application/json",
"x-safe": "visible",
},
response: new Response("{}", {
status: 200,
headers: {
"content-type": "application/json",
"set-cookie": "sid=response-token",
},
}),
});
await new Promise((resolve) => setImmediate(resolve));
finalizeDebugProxyCapture();
const request = storeState.events.find((event) => event.kind === "request");
expect(JSON.parse(String(request?.headersJson))).toMatchObject({
Authorization: "[REDACTED]",
Cookie: "[REDACTED]",
"x-api-key": "[REDACTED]",
"content-type": "application/json",
"x-safe": "visible",
});
const response = storeState.events.find((event) => event.kind === "response");
expect(JSON.parse(String(response?.headersJson))).toMatchObject({
"content-type": "application/json",
"set-cookie": "[REDACTED]",
});
});
});

View File

@@ -15,6 +15,29 @@ import type {
} from "./types.js";
const DEBUG_PROXY_FETCH_PATCH_KEY = Symbol.for("openclaw.debugProxy.fetchPatch");
const REDACTED_CAPTURE_HEADER_VALUE = "[REDACTED]";
const SENSITIVE_CAPTURE_HEADER_NAMES = new Set([
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"x-api-key",
"api-key",
"apikey",
"x-auth-token",
"auth-token",
"x-access-token",
"access-token",
]);
const SENSITIVE_CAPTURE_HEADER_NAME_FRAGMENTS = [
"api-key",
"apikey",
"token",
"secret",
"password",
"credential",
"session",
];
type GlobalFetchPatchedState = {
originalFetch: typeof globalThis.fetch;
@@ -55,6 +78,32 @@ function resolveUrlString(input: RequestInfo | URL): string | null {
return null;
}
function isSensitiveCaptureHeaderName(name: string): boolean {
const normalized = name.trim().toLowerCase();
if (!normalized) {
return false;
}
if (SENSITIVE_CAPTURE_HEADER_NAMES.has(normalized)) {
return true;
}
return SENSITIVE_CAPTURE_HEADER_NAME_FRAGMENTS.some((fragment) => normalized.includes(fragment));
}
function redactedCaptureHeaders(
headers: Headers | Record<string, string> | undefined,
): Record<string, string> | undefined {
if (!headers) {
return undefined;
}
const entries =
headers instanceof Headers ? Array.from(headers.entries()) : Object.entries(headers);
const redacted: Record<string, string> = {};
for (const [name, value] of entries) {
redacted[name] = isSensitiveCaptureHeaderName(name) ? REDACTED_CAPTURE_HEADER_VALUE : value;
}
return redacted;
}
function createHttpCaptureEventBase(params: {
settings: DebugProxySettings;
rawUrl: string;
@@ -237,11 +286,7 @@ export function captureHttpExchange(params: {
params.requestHeaders instanceof Headers
? (params.requestHeaders.get("content-type") ?? undefined)
: params.requestHeaders?.["content-type"],
headersJson: safeJsonString(
params.requestHeaders instanceof Headers
? Object.fromEntries(params.requestHeaders.entries())
: params.requestHeaders,
),
headersJson: safeJsonString(redactedCaptureHeaders(params.requestHeaders)),
metaJson: safeJsonString(params.meta),
...requestPayload,
});
@@ -268,7 +313,7 @@ export function captureHttpExchange(params: {
: undefined,
headersJson:
params.response.headers && typeof params.response.headers.entries === "function"
? safeJsonString(Object.fromEntries(params.response.headers.entries()))
? safeJsonString(redactedCaptureHeaders(params.response.headers))
: undefined,
metaJson: safeJsonString({ ...params.meta, bodyCapture: "unavailable" }),
});
@@ -295,7 +340,7 @@ export function captureHttpExchange(params: {
}),
status: params.response.status,
contentType: params.response.headers.get("content-type") ?? undefined,
headersJson: safeJsonString(Object.fromEntries(params.response.headers.entries())),
headersJson: safeJsonString(redactedCaptureHeaders(params.response.headers)),
metaJson: safeJsonString(params.meta),
...responsePayload,
});