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,16 +1,12 @@
|
||||
import { isSensitiveKey, REDACTED, redactString } from "@umt/contracts";
|
||||
import pino, { type Logger } from "pino";
|
||||
|
||||
export const REDACTED = "[REDACTED]";
|
||||
// 键表与 URL query / 请求头 / Bearer 规则与 Agent 的 redactForModel 共用一份(@umt/contracts redact.ts):
|
||||
// 键名规范化后命中 passkey / rsskey / torrent_pass / authkey / downhash / c_secure_pass / cookie / x-api-key / *token …
|
||||
// 即整体替换(数值型如 token 计数除外);其余字符串按 redactString 处理。
|
||||
export { REDACTED, redactString };
|
||||
|
||||
// 字段名包含这些词即视为敏感(数值型如 token 计数除外)
|
||||
const SENSITIVE_KEY = /passkey|token|cookie|authorization|password|secret|api[-_]?key/i;
|
||||
// URL query 中的敏感参数
|
||||
const SENSITIVE_QUERY = /([?&](?:passkey|token|access_token|api_key|apikey|auth|sign)=)[^&#\s"'<>]*/gi;
|
||||
|
||||
/** 脱敏字符串中的 URL query 参数 */
|
||||
export function redactString(s: string): string {
|
||||
return s.replace(SENSITIVE_QUERY, `$1${REDACTED}`);
|
||||
}
|
||||
const sensitive = (k: string, v: unknown) => isSensitiveKey(k) && typeof v !== "number" && v != null;
|
||||
|
||||
/** 递归脱敏:敏感字段整体替换,其余字符串做 URL query 脱敏 */
|
||||
export function redact(value: unknown, depth = 0): unknown {
|
||||
@@ -21,7 +17,7 @@ export function redact(value: unknown, depth = 0): unknown {
|
||||
if (value && typeof value === "object") {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
if (SENSITIVE_KEY.test(k) && typeof v !== "number" && v != null) out[k] = REDACTED;
|
||||
if (sensitive(k, v)) out[k] = REDACTED;
|
||||
else out[k] = redact(v, depth + 1);
|
||||
}
|
||||
return out;
|
||||
@@ -37,7 +33,7 @@ function redactError(err: Error, depth: number): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = { type: err.name, message: redactString(err.message) };
|
||||
if (err.stack) out.stack = redactString(err.stack);
|
||||
for (const [k, v] of Object.entries(err)) {
|
||||
out[k] = SENSITIVE_KEY.test(k) && typeof v !== "number" && v != null ? REDACTED : redact(v, depth + 1);
|
||||
out[k] = sensitive(k, v) ? REDACTED : redact(v, depth + 1);
|
||||
}
|
||||
if (err.cause !== undefined) out.cause = redact(err.cause, depth + 1);
|
||||
return out;
|
||||
|
||||
Reference in new issue
Block a user