- contracts: isSensitiveQueryParam (whole-name match on SENSITIVE_QUERY_PARAMS or isSensitiveKey), the single source for query credential names - rss/redact: drop local SENSITIVE_QUERY_KEYS; param checks go through isSensitiveQueryParam (+ caller extras); REDACTED re-exported from contracts - browser/redact: CREDENTIAL_PARAM built from SENSITIVE_QUERY_PARAMS, JSON keys also checked with isSensitiveKey; keeps only its own rules (path passkey, cookie names, a11y password fields); remove stray regex statement - redact-consistency.test: 24 credential spellings (pass_key, c_secure_pass, refresh_token, password, uid_key, auth, ...) masked by contracts, rss and browser alike; plain params untouched; rss extract/unredact round-trip Co-Authored-By: Claude Opus 5.5 <[email protected]>
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
/**
|
|
* 凭证脱敏的唯一键表与字符串规则(服务端 logger、Agent 模型上下文、适配器错误信息共用)。
|
|
* 覆盖 cookie / passkey / rsskey / torrent_pass / authkey / downhash / c_secure_pass / token / apiKey /
|
|
* authorization / x-api-key 头 / URL 内嵌账号密码。键名先规范化(小写、去掉 - _ 空白)再判断,
|
|
* 所以 PassKey、pass-key、pass_key、X-Api-Key 都能命中。
|
|
* 宁可多遮不可漏遮:误伤只影响可读性,漏遮会把凭证送进日志或外部模型。
|
|
*/
|
|
export const REDACTED = "[REDACTED]";
|
|
|
|
/** 规范化后的键名只要包含其中一项即视为敏感 */
|
|
export const SENSITIVE_KEY_PARTS: readonly string[] = [
|
|
"cookie",
|
|
"passkey",
|
|
"authkey",
|
|
"torrentpass",
|
|
"securepass",
|
|
"rsskey",
|
|
"downhash",
|
|
"apikey",
|
|
"authorization",
|
|
"secret",
|
|
"password",
|
|
"passwd",
|
|
"credential",
|
|
"privatekey",
|
|
];
|
|
|
|
/** 规范化键名(去掉 - _ 空白并小写)后判断。以 token 结尾算敏感,tokens(用量统计)不算。 */
|
|
export function isSensitiveKey(key: string): boolean {
|
|
const k = key.toLowerCase().replace(/[-_\s]/g, "");
|
|
if (k.endsWith("token")) return true;
|
|
if (k === "sign" || k === "signature") return true;
|
|
return SENSITIVE_KEY_PARTS.some((part) => k.includes(part));
|
|
}
|
|
|
|
/** URL 查询参数/片段中视为凭证的参数名(正则片段,大小写不敏感;- _ 可选的写成 [_-]?) */
|
|
export const SENSITIVE_QUERY_PARAMS: readonly string[] = [
|
|
"pass[_-]?key",
|
|
"auth[_-]?key",
|
|
"torrent[_-]?pass",
|
|
"rss[_-]?key",
|
|
"c[_-]?secure[_-]?pass",
|
|
"down[_-]?hash",
|
|
"api[_-]?key",
|
|
"access[_-]?token",
|
|
"refresh[_-]?token",
|
|
"token",
|
|
"auth",
|
|
"sign",
|
|
"signature",
|
|
"credential",
|
|
"secret",
|
|
"password",
|
|
"uid[_-]?key",
|
|
"key",
|
|
];
|
|
|
|
const PARAM_NAME_RE = new RegExp(`^(?:${SENSITIVE_QUERY_PARAMS.join("|")})$`, "i");
|
|
|
|
/**
|
|
* 单个 URL 查询参数名是否为凭证(整名匹配 SENSITIVE_QUERY_PARAMS,或按 isSensitiveKey 的键名规则命中)。
|
|
* rss / browser 等适配器的参数判定都走这里,不再各自维护键表(验收 07 S-4)。
|
|
*/
|
|
export function isSensitiveQueryParam(name: string): boolean {
|
|
return PARAM_NAME_RE.test(name) || isSensitiveKey(name);
|
|
}
|
|
|
|
const PARAM_RE = new RegExp(`([?&;#]|\\b)(${SENSITIVE_QUERY_PARAMS.join("|")})=([^&;#\\s"'<>]+)`, "gi");
|
|
const URL_USERINFO_RE = /\b([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^/\s@]+)@/gi;
|
|
const BEARER_RE = /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{6,}/g;
|
|
const HEADER_RE = /\b(cookie|set-cookie|authorization|x-api-key)\s*:\s*[^\r\n]+/gi;
|
|
const KNOWN_KEY_RE = /\b(sk-[A-Za-z0-9_-]{8,}|sk-ant-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{16,})\b/g;
|
|
|
|
/** 文本脱敏:请求头行、URL userinfo、query 凭证参数、Bearer/Basic、已知格式的密钥 */
|
|
export function redactString(input: string): string {
|
|
return input
|
|
.replace(HEADER_RE, (_m, name: string) => `${name}: ${REDACTED}`)
|
|
.replace(URL_USERINFO_RE, (_m, scheme: string) => `${scheme}${REDACTED}@`)
|
|
.replace(PARAM_RE, (_m, lead: string, name: string) => `${lead}${name}=${REDACTED}`)
|
|
.replace(BEARER_RE, (_m, scheme: string) => `${scheme} ${REDACTED}`)
|
|
.replace(KNOWN_KEY_RE, REDACTED);
|
|
}
|
|
|
|
/**
|
|
* URL(或含 URL 的错误信息)脱敏。`secrets` 为调用方已知的明文凭证,出现在任何位置都会被替换
|
|
* (兜底 fetch 实现把 URL 编码后再拼进错误信息之类的情况)。
|
|
*/
|
|
export function redactUrl(input: string, secrets: readonly (string | undefined | null)[] = []): string {
|
|
let out = redactString(input);
|
|
for (const s of secrets) {
|
|
if (!s || s.length < 4) continue;
|
|
out = out.split(s).join(REDACTED);
|
|
const enc = encodeURIComponent(s);
|
|
if (enc !== s) out = out.split(enc).join(REDACTED);
|
|
}
|
|
return out;
|
|
}
|