From f7bc9fd5fb55e99e634cc81a3439498ed8c8c8d7 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Mon, 13 Jul 2026 09:17:01 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20SSRF=20=E5=8A=A0=E5=9B=BA=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20=E2=80=94=20=E6=8B=92=E7=BB=9D=E5=86=85=E7=BD=91/?= =?UTF-8?q?=E5=9B=9E=E7=8E=AF/=E9=93=BE=E8=B7=AF=E6=9C=AC=E5=9C=B0=20IPv4?= =?UTF-8?q?=20=E9=82=AE=E7=AE=B1=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=9C=B0?= =?UTF-8?q?=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 响应 push 安全审查:settings VALIDATORS 的 isValidHost 增加内网 IPv4 字面量 检测,拒绝 0/8、10/8、127/8(回环)、169.254/16(链路本地/云元数据)、 172.16/12、192.168/16、100.64/10(CGNAT),防止 imap/pop3/smtp 服务器地址 被指向内网主机(SSRF 纵深防御)。 未做 DNS 解析 + rebinding 防御:host 需 monitor:write 权限修改、且为 POP3/TLS 协议连接(非 HTTP),对 admin-only 邮箱配置属过度设计。IPv6 字面量含 ':' 已被字符集正则天然拒绝。 --- src/app/api/monitor/settings/route.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/app/api/monitor/settings/route.ts b/src/app/api/monitor/settings/route.ts index b9d4861..9c62264 100644 --- a/src/app/api/monitor/settings/route.ts +++ b/src/app/api/monitor/settings/route.ts @@ -5,11 +5,28 @@ import { getCurrentUser } from '@/lib/auth' import { hasPermission } from '@/lib/permissions' import { getMaskedConfig, updateMonitorConfig } from '@/lib/monitor/settings-manager' import { writeAuditLog, getClientIP } from '@/lib/audit' +import net from 'net' -// 主机名格式校验(纵深防御):只允许合法主机名/IP 字符,拒绝 URL/路径/空格等, -// 防止误配或配置被篡改后连接到任意主机(SSRF 面收窄) -const isValidHost = (v: unknown): boolean => - typeof v === 'string' && v.length > 0 && v.length <= 253 && /^[a-zA-Z0-9.-]+$/.test(v) +// 拒绝内网/回环/链路本地/CGNAT 的 IPv4 字面量(SSRF 纵深防御,防收信指向内网主机) +function isPrivateIPv4(host: string): boolean { + if (!net.isIPv4(host)) return false + const p = host.split('.').map(Number) + if (p[0] === 0 || p[0] === 10 || p[0] === 127) return true // 0/8, 10/8, 回环 127/8 + if (p[0] === 169 && p[1] === 254) return true // 链路本地 169.254/16 + if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true // 172.16/12 + if (p[0] === 192 && p[1] === 168) return true // 192.168/16 + if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true // CGNAT 100.64/10 + return false +} + +// 主机名格式校验(纵深防御):只允许合法主机名/IP 字符(IPv6 字面量含 ':' 天然被拒), +// 拒绝 URL/路径/空格及内网 IPv4,收窄 SSRF 面 +const isValidHost = (v: unknown): boolean => { + if (typeof v !== 'string' || v.length === 0 || v.length > 253) return false + if (!/^[a-zA-Z0-9.-]+$/.test(v)) return false + if (isPrivateIPv4(v)) return false + return true +} const VALIDATORS: Record boolean> = { 'monitor.enabled': (v) => typeof v === 'boolean',