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:
@@ -12,6 +12,10 @@ THREAT_TYPE_SQL_INJECTION = "sql_injection"
|
||||
THREAT_TYPE_XSS = "xss"
|
||||
THREAT_TYPE_PATH_TRAVERSAL = "path_traversal"
|
||||
THREAT_TYPE_COMMAND_INJECTION = "command_injection"
|
||||
THREAT_TYPE_SSRF = "ssrf"
|
||||
THREAT_TYPE_XXE = "xxe"
|
||||
THREAT_TYPE_TEMPLATE_INJECTION = "template_injection"
|
||||
THREAT_TYPE_SENSITIVE_PATH_PROBE = "sensitive_path_probe"
|
||||
|
||||
|
||||
# ==================== Scores ====================
|
||||
@@ -23,6 +27,10 @@ SCORE_SQL_INJECTION = 90
|
||||
SCORE_XSS = 70
|
||||
SCORE_PATH_TRAVERSAL = 60
|
||||
SCORE_COMMAND_INJECTION = 85
|
||||
SCORE_SSRF = 75
|
||||
SCORE_XXE = 85
|
||||
SCORE_TEMPLATE_INJECTION = 70
|
||||
SCORE_SENSITIVE_PATH_PROBE = 40
|
||||
|
||||
|
||||
# ==================== JNDI (Log4j) ====================
|
||||
@@ -75,6 +83,33 @@ CMD_INJECTION_OPERATOR_WITH_CMD_PATTERN = (
|
||||
CMD_INJECTION_SUBSHELL_PATTERN = r"(?:`[^`]{1,200}`|\$\([^)]{1,200}\))"
|
||||
|
||||
|
||||
# ==================== SSRF ====================
|
||||
|
||||
SSRF_LOCALHOST_URL_PATTERN = r"\bhttps?\s*:\s*//\s*(?:127\.0\.0\.1\b|localhost\b|0\.0\.0\.0\b)"
|
||||
SSRF_INTERNAL_IP_URL_PATTERN = r"\bhttps?\s*:\s*//\s*(?:10\.|192\.168\.|172\.(?:1[6-9]|2[0-9]|3[0-1])\.)"
|
||||
SSRF_DANGEROUS_PROTOCOL_PATTERN = r"\b(?:file|gopher|dict)\s*:\s*//"
|
||||
|
||||
|
||||
# ==================== XXE ====================
|
||||
|
||||
XXE_DOCTYPE_PATTERN = r"<!\s*doctype\b|\bdoctype\b"
|
||||
XXE_ENTITY_PATTERN = r"<!\s*entity\b|\bentity\b"
|
||||
XXE_SYSTEM_PUBLIC_PATTERN = r"\b(?:system|public)\b"
|
||||
|
||||
|
||||
# ==================== Template Injection ====================
|
||||
|
||||
TEMPLATE_JINJA_EXPR_PATTERN = r"\{\{\s*[^}]{0,200}\s*\}\}"
|
||||
TEMPLATE_JINJA_STMT_PATTERN = r"\{%\s*[^%]{0,200}\s*%\}"
|
||||
TEMPLATE_VELOCITY_DIRECTIVE_PATTERN = r"#\s*(?:set|if)\b"
|
||||
|
||||
|
||||
# ==================== Sensitive Path Probing ====================
|
||||
|
||||
SENSITIVE_PATH_DOTFILES_PATTERN = r"/\.(?:git|svn|env)(?:/|\b|$)"
|
||||
SENSITIVE_PATH_PROBE_PATTERN = r"/(?:actuator|phpinfo|wp-admin)(?:/|\b|$)"
|
||||
|
||||
|
||||
# ==================== Compiled Regex ====================
|
||||
|
||||
_FLAGS = re.IGNORECASE | re.MULTILINE
|
||||
@@ -95,3 +130,17 @@ PATH_TRAVERSAL_RE = re.compile(PATH_TRAVERSAL_PATTERN, _FLAGS)
|
||||
CMD_INJECTION_OPERATOR_WITH_CMD_RE = re.compile(CMD_INJECTION_OPERATOR_WITH_CMD_PATTERN, _FLAGS)
|
||||
CMD_INJECTION_SUBSHELL_RE = re.compile(CMD_INJECTION_SUBSHELL_PATTERN, _FLAGS)
|
||||
|
||||
SSRF_LOCALHOST_URL_RE = re.compile(SSRF_LOCALHOST_URL_PATTERN, _FLAGS)
|
||||
SSRF_INTERNAL_IP_URL_RE = re.compile(SSRF_INTERNAL_IP_URL_PATTERN, _FLAGS)
|
||||
SSRF_DANGEROUS_PROTOCOL_RE = re.compile(SSRF_DANGEROUS_PROTOCOL_PATTERN, _FLAGS)
|
||||
|
||||
XXE_DOCTYPE_RE = re.compile(XXE_DOCTYPE_PATTERN, _FLAGS)
|
||||
XXE_ENTITY_RE = re.compile(XXE_ENTITY_PATTERN, _FLAGS)
|
||||
XXE_SYSTEM_PUBLIC_RE = re.compile(XXE_SYSTEM_PUBLIC_PATTERN, _FLAGS)
|
||||
|
||||
TEMPLATE_JINJA_EXPR_RE = re.compile(TEMPLATE_JINJA_EXPR_PATTERN, _FLAGS)
|
||||
TEMPLATE_JINJA_STMT_RE = re.compile(TEMPLATE_JINJA_STMT_PATTERN, _FLAGS)
|
||||
TEMPLATE_VELOCITY_DIRECTIVE_RE = re.compile(TEMPLATE_VELOCITY_DIRECTIVE_PATTERN, _FLAGS)
|
||||
|
||||
SENSITIVE_PATH_DOTFILES_RE = re.compile(SENSITIVE_PATH_DOTFILES_PATTERN, _FLAGS)
|
||||
SENSITIVE_PATH_PROBE_RE = re.compile(SENSITIVE_PATH_PROBE_PATTERN, _FLAGS)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user