fix(security): W4-E1 approvals admin-session only, shared redact table, TMDB key redaction, prod mock gate
- POST /approvals and PATCH /approvals/{id} require a console admin session
(EndpointDef auth:"session"); API keys incl. admin scope get 403, so an
organize.execute key can no longer approve its own plan (S1)
- tests for single-use approval checks: expired, revision/payload hash
mismatch, consumed, revoked, pending/rejected, plan mismatch; re-approve
after plan change succeeds once and cannot be replayed (S4)
- one redact key table in @umt/contracts shared by server logger and agent
redactForModel: rsskey, torrent_pass, authkey, downhash, c_secure_pass,
passkey variants, x-api-key, cookie headers, nested objects/arrays (S2)
- TMDB network errors: message and cause redacted via redactUrl (S3)
- web Agent ?mock=1 only in DEV builds or when pi.provider is fake (S7)
- EndpointDef auth annotations for /auth/*, asserted against
PUBLIC_AUTH_PATHS (S10); doc drift fixes (S11)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
79cdd26457
commit
f48e1aac96
25 files changed
+825
-235
No files matched your search
@@ -1,50 +1,12 @@
|
||||
/**
|
||||
* 敏感信息守卫:工具输入输出进入模型上下文、事件流、审计前统一经过这里。
|
||||
* 覆盖 cookie / passkey / token / apiKey / authorization / 含 passkey 等私密参数的 URL / URL 内嵌账号密码。
|
||||
* 键表见 @umt/contracts 的 redact.ts(cookie / passkey / rsskey / torrent_pass / authkey / downhash / c_secure_pass / token / apiKey …)。
|
||||
* 宁可多遮不可漏遮:误伤只影响可读性,漏遮会把凭证送给外部模型。
|
||||
*/
|
||||
export const REDACTED = "[REDACTED]";
|
||||
import { isSensitiveKey, REDACTED, redactString } from "@umt/contracts";
|
||||
|
||||
const SENSITIVE_KEY_PARTS = [
|
||||
"cookie",
|
||||
"passkey",
|
||||
"authkey",
|
||||
"torrentpass",
|
||||
"rsskey",
|
||||
"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 查询参数/片段中视为凭证的参数名 */
|
||||
const SENSITIVE_PARAM =
|
||||
"passkey|authkey|torrent_pass|rsskey|api[_-]?key|access[_-]?token|refresh[_-]?token|token|auth|sign|signature|credential|secret|password|key|uid_key|downhash";
|
||||
const PARAM_RE = new RegExp(`([?&;#]|\\b)(${SENSITIVE_PARAM})=([^&;#\\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;
|
||||
|
||||
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);
|
||||
}
|
||||
// 键表与字符串规则只保留一份,在 @umt/contracts(redact.ts);服务端 logger 与这里共用。
|
||||
export { isSensitiveKey, REDACTED, redactString };
|
||||
|
||||
/** 深度脱敏,返回新对象;不修改入参。循环引用替换为 "[Circular]"。 */
|
||||
export function redactForModel<T>(value: T): T {
|
||||
|
||||
Reference in new issue
Block a user