ForcePilot/backend/package/yuxi/channel/security/log_sanitizer.py
Kris 849d21120f feat(channel/security): 新增完整安全工具模块
新增一系列安全相关功能:
1.  新增二维码生成工具,支持自定义参数导出图片/Base64/字节流
2.  新增HTML内容安全处理工具,包括标签剥离、转义、URL校验
3.  新增日志敏感信息脱敏工具,支持配置、字典、日志记录脱敏
4.  新增密钥管理运行时,支持从环境变量/文件加载密钥
5.  新增身份链接管理,支持多渠道身份绑定与解析
6.  新增SSRF防护工具,支持域名/IP校验与固定主机
7.  新增安全权限修复工具,修复文件目录权限与配置项
8.  新增外部内容安全处理,支持LLM特殊令牌剥离与注入检测
9.  新增认证限流工具,支持多维度限流与本地回环豁免
10. 新增配对管理工具,支持安全配对码生成与校验
11. 新增白名单管理工具,支持DM/群组/来源白名单校验
2026-05-21 10:32:57 +08:00

107 lines
3.0 KiB
Python

import re
from typing import Any
ENV_STYLE_PATTERN = re.compile(
r"(?i)(api[_-]?key|token|secret|password|passwd|credential|private[_-]?key)\s*[:=]\s*([\"\']?[^\s,\"\']+[\"\']?)",
)
URL_CREDENTIAL_PATTERN = re.compile(r"://([^:@]+):([^@]+)@")
JWT_PATTERN = re.compile(r"(eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.\S+)")
AUTH_HEADER_PATTERN = re.compile(r"(bearer\s+|basic\s+|api[_-]?key\s+)([a-zA-Z0-9+/=_-]+)", re.IGNORECASE)
REDACTED = "***"
MAX_SANITIZE_DEPTH = 20
SENSITIVE_CONFIG_KEYS = frozenset(
{
"app_secret",
"bot_token",
"signing_secret",
"encrypt_key",
"verification_token",
"api_key",
"access_token",
"refresh_token",
"private_key",
"webhook_secret",
"client_secret",
"secret",
"password",
"passwd",
"auth_token",
"jwt",
"credential",
"app_key",
"master_key",
"license_key",
}
)
_SENSITIVE_KEYWORD_RE = re.compile(
r"(?:^|[_-])("
r"app[_-]?secret|bot[_-]?token|signing[_-]?secret|encrypt[_-]?key|"
r"verification[_-]?token|api[_-]?key|access[_-]?token|refresh[_-]?token|"
r"private[_-]?key|webhook[_-]?secret|client[_-]?secret|secret|"
r"password|passwd|auth[_-]?token|jwt|credential|"
r"app[_-]?key|master[_-]?key|license[_-]?key"
r")(?:$|[_-])"
)
def _is_sensitive_key(key: str) -> bool:
key_lower = key.lower().replace("-", "_")
if key_lower in SENSITIVE_CONFIG_KEYS:
return True
return bool(_SENSITIVE_KEYWORD_RE.search(key_lower))
def sanitize_text(text: str) -> str:
text = ENV_STYLE_PATTERN.sub(r"\1=***", text)
text = URL_CREDENTIAL_PATTERN.sub(r"://\1:***@", text)
text = JWT_PATTERN.sub(REDACTED, text)
text = AUTH_HEADER_PATTERN.sub(r"\1***", text)
return text
def sanitize_dict(data: dict, _depth: int = 0) -> dict:
if _depth > MAX_SANITIZE_DEPTH:
return {"__redacted__": REDACTED}
result = {}
for key, value in data.items():
result[key] = _sanitize_value(key, value, _depth)
return result
def _sanitize_value(key: str, value: Any, _depth: int = 0) -> Any:
if _depth > MAX_SANITIZE_DEPTH:
return REDACTED
if isinstance(value, dict):
return sanitize_dict(value, _depth + 1)
if isinstance(value, list):
return [_sanitize_value("", item, _depth + 1) for item in value]
if isinstance(value, str):
if _is_sensitive_key(key):
return REDACTED
return sanitize_text(value)
return value
def sanitize_log_record(record_data: dict) -> dict:
return sanitize_dict(record_data)
def sanitize_for_json_dump(obj: Any, _depth: int = 0) -> Any:
if _depth > MAX_SANITIZE_DEPTH:
return REDACTED
if isinstance(obj, dict):
return {k: sanitize_for_json_dump(v, _depth + 1) for k, v in obj.items()}
if isinstance(obj, list):
return [sanitize_for_json_dump(v, _depth + 1) for v in obj]
if isinstance(obj, str):
return sanitize_text(obj)
return obj
sanitize_config_object = sanitize_dict