mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 20:30:42 +00:00
fix(diagnostics): harden capture redaction and discord metadata fetch (#71303)
This commit is contained in:
@@ -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]",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user