mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
8b21c23cc2
* feat(web-fetch): add tools.web.fetch.headers for operator request headers * docs(web-fetch): update cache discriminator comment * fix(web-fetch): reserve runtime and cookie headers * fix(web-fetch): harden operator header normalization * fix(web-fetch): align header safety contracts * fix(web-fetch): close header logging gaps * fix(web-fetch): report case-colliding headers * fix(web-fetch): reject stale colliding headers * fix(web-fetch): refuse credential token aliases * fix(web-fetch): preserve empty header values * fix(web-fetch): refuse credential-shaped headers * chore(config): refresh web fetch header baselines * test(web-fetch): cover header security contracts * fix(web-fetch): narrow credential header refusal * fix(web-fetch): preserve trace metadata headers * fix(web-fetch): keep header validation internal * test(web-fetch): satisfy strict test contracts * fix(web-fetch): refuse vendor credential headers * fix(web-fetch): refuse authentication signatures * fix(web-fetch): detect qualified auth signatures * fix(web-fetch): refuse auth-suffixed headers * fix(web-fetch): refuse compact credential headers * fix(secrets): audit authentication signatures * fix(web-fetch): redact operator headers in captures * fix(web-fetch): keep capture metadata internal * fix(web-fetch): allow sensitive operator headers * test(web-fetch): cover normalized operator headers * docs: note web fetch request headers * chore(config): refresh web fetch header baseline * chore: remove release-owned changelog entry
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
/**
|
|
* Canonical header redaction for debug proxy captures.
|
|
*
|
|
* Both capture writers — the patched-fetch runtime and the standalone proxy
|
|
* server — must redact identically. A capture that leaks credentials is worse
|
|
* than no capture, and the standalone path previously stored raw headers while
|
|
* the runtime path redacted, so this policy lives in one leaf module that both
|
|
* import rather than being duplicated per writer.
|
|
*/
|
|
import { redactRegisteredSecretValues } from "../logging/secret-redaction-registry.js";
|
|
|
|
export 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",
|
|
];
|
|
|
|
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));
|
|
}
|
|
|
|
export function redactedCaptureHeaders(
|
|
headers: Headers | Record<string, string | string[] | undefined> | undefined,
|
|
additionalSensitiveNames?: Iterable<string>,
|
|
): Record<string, string> | undefined {
|
|
if (!headers) {
|
|
return undefined;
|
|
}
|
|
const additionalSensitive = new Set(
|
|
[...(additionalSensitiveNames ?? [])].map((name) => name.trim().toLowerCase()),
|
|
);
|
|
const entries =
|
|
headers instanceof Headers ? Array.from(headers.entries()) : Object.entries(headers);
|
|
const redacted: Record<string, string> = {};
|
|
for (const [name, value] of entries) {
|
|
// Header names are matched exactly and by sensitive fragments because
|
|
// providers use many token/key naming variants. Names that pass the check
|
|
// still run through value redaction so a registered secret pasted into an
|
|
// innocuous header does not survive.
|
|
if (additionalSensitive.has(name.trim().toLowerCase()) || isSensitiveCaptureHeaderName(name)) {
|
|
redacted[name] = REDACTED_CAPTURE_HEADER_VALUE;
|
|
continue;
|
|
}
|
|
const flattened = Array.isArray(value) ? value.join(", ") : (value ?? "");
|
|
redacted[name] = redactRegisteredSecretValues(flattened, () => REDACTED_CAPTURE_HEADER_VALUE);
|
|
}
|
|
return redacted;
|
|
}
|