Files
openclaw/src/proxy-capture/header-redaction.ts
T
Jesse Merhi 8b21c23cc2 feat(web-fetch): add tools.web.fetch.headers for operator request headers (#115545)
* 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
2026-08-02 02:07:25 +10:00

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;
}