feat: 安全增强 + 删除密码重置申请功能 + 登录提醒开关

安全增强:
- 新增 SSRF、XXE、模板注入、敏感路径探测检测规则
- security/constants.py: 添加新的威胁类型和检测模式
- security/threat_detector.py: 实现新检测逻辑

删除密码重置申请功能:
- 移除 /api/password_resets 相关API
- 删除 password_reset_requests 数据库表
- 前端移除密码重置申请页面和菜单
- 用户只能通过邮��找回密码,未绑定邮箱需联系管理员

登录提醒全局开关:
- email_service.py: 添加 login_alert_enabled 字段
- routes/api_auth.py: 检查开关状态再发送登录提醒
- EmailPage.vue: 添加新设备登录提醒开关

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-27 12:08:36 +08:00
parent 4ba933b001
commit 89f3fd9759
65 changed files with 555 additions and 784 deletions

View File

@@ -71,6 +71,10 @@ class ThreatDetector:
self._check_xss,
self._check_path_traversal,
self._check_command_injection,
self._check_ssrf,
self._check_xxe,
self._check_template_injection,
self._check_sensitive_path_probe,
]:
result = check(text)
if result:
@@ -168,6 +172,96 @@ class ThreatDetector:
return (C.THREAT_TYPE_COMMAND_INJECTION, C.SCORE_COMMAND_INJECTION, "CMD_OPERATOR_WITH_CMD", m.group(0))
return None
def _check_ssrf(self, text: str) -> Optional[Tuple[str, int, str, str]]:
decoded = self._multi_unquote(text)
candidates: List[Tuple[str, str]] = [(text, "")]
if decoded != text:
candidates.append((decoded, "_URL_DECODED"))
for candidate, suffix in candidates:
m = C.SSRF_LOCALHOST_URL_RE.search(candidate)
if m:
return (C.THREAT_TYPE_SSRF, C.SCORE_SSRF, f"SSRF_LOCALHOST{suffix}", m.group(0))
m = C.SSRF_INTERNAL_IP_URL_RE.search(candidate)
if m:
return (C.THREAT_TYPE_SSRF, C.SCORE_SSRF, f"SSRF_INTERNAL_IP{suffix}", m.group(0))
m = C.SSRF_DANGEROUS_PROTOCOL_RE.search(candidate)
if m:
return (C.THREAT_TYPE_SSRF, C.SCORE_SSRF, f"SSRF_DANGEROUS_PROTOCOL{suffix}", m.group(0))
return None
def _check_xxe(self, text: str) -> Optional[Tuple[str, int, str, str]]:
decoded = self._multi_unquote(text)
candidates: List[Tuple[str, str]] = [(text, "")]
if decoded != text:
candidates.append((decoded, "_URL_DECODED"))
for candidate, suffix in candidates:
m_doctype = C.XXE_DOCTYPE_RE.search(candidate)
if not m_doctype:
continue
m_entity = C.XXE_ENTITY_RE.search(candidate)
if not m_entity:
continue
m_sys_pub = C.XXE_SYSTEM_PUBLIC_RE.search(candidate)
if not m_sys_pub:
continue
matched = f"{m_doctype.group(0)} {m_entity.group(0)} {m_sys_pub.group(0)}"
return (C.THREAT_TYPE_XXE, C.SCORE_XXE, f"XXE_KEYWORD_COMBO{suffix}", matched)
return None
def _check_template_injection(self, text: str) -> Optional[Tuple[str, int, str, str]]:
decoded = self._multi_unquote(text)
candidates: List[Tuple[str, str]] = [(text, "")]
if decoded != text:
candidates.append((decoded, "_URL_DECODED"))
for candidate, suffix in candidates:
m = C.TEMPLATE_JINJA_EXPR_RE.search(candidate)
if m:
return (C.THREAT_TYPE_TEMPLATE_INJECTION, C.SCORE_TEMPLATE_INJECTION, f"TEMPLATE_JINJA_EXPR{suffix}", m.group(0))
m = C.TEMPLATE_JINJA_STMT_RE.search(candidate)
if m:
return (C.THREAT_TYPE_TEMPLATE_INJECTION, C.SCORE_TEMPLATE_INJECTION, f"TEMPLATE_JINJA_STMT{suffix}", m.group(0))
m = C.TEMPLATE_VELOCITY_DIRECTIVE_RE.search(candidate)
if m:
return (
C.THREAT_TYPE_TEMPLATE_INJECTION,
C.SCORE_TEMPLATE_INJECTION,
f"TEMPLATE_VELOCITY_DIRECTIVE{suffix}",
m.group(0),
)
return None
def _check_sensitive_path_probe(self, text: str) -> Optional[Tuple[str, int, str, str]]:
decoded = self._multi_unquote(text)
candidates: List[Tuple[str, str]] = [(text, "")]
if decoded != text:
candidates.append((decoded, "_URL_DECODED"))
for candidate, suffix in candidates:
m = C.SENSITIVE_PATH_DOTFILES_RE.search(candidate)
if m:
return (
C.THREAT_TYPE_SENSITIVE_PATH_PROBE,
C.SCORE_SENSITIVE_PATH_PROBE,
f"SENSITIVE_PATH_DOTFILES{suffix}",
m.group(0),
)
m = C.SENSITIVE_PATH_PROBE_RE.search(candidate)
if m:
return (
C.THREAT_TYPE_SENSITIVE_PATH_PROBE,
C.SCORE_SENSITIVE_PATH_PROBE,
f"SENSITIVE_PATH_PROBE{suffix}",
m.group(0),
)
return None
# ==================== Helpers ====================
def _preview(self, text: str, limit: int = 160) -> str: