From 15ea15518c11a9f3a61bf240f5bb362b09ecf399 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Thu, 27 Nov 2025 19:42:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E4=BF=AE=E5=A4=8D=E4=BF=A1?= =?UTF-8?q?=E4=BB=BB=E4=BB=A3=E7=90=86=E5=92=8CHTTPS=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E7=9A=84=E5=AE=89=E5=85=A8=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题修复 1. **trust proxy 配置安全加固** - 默认改为 false(不信任任何代理) - 支持多种安全配置:数字跳数、loopback、IP/CIDR段 - 当配置为 true 时输出安全警告 2. **HTTPS 检测基于可信代理链** - 使用 req.secure 替代直接读取 x-forwarded-proto - Express 会根据 trust proxy 配置判断是否采信代理头 - 防止客户端伪造协议头绕过 HTTPS 强制 3. **客户端 IP 获取安全加固** - 使用 req.ip 替代直接读取 X-Forwarded-For - Express 会根据 trust proxy 配置正确处理代理链 - 防止客户端伪造 IP 绕过限流 4. **健康检测增加安全警告** - trust proxy = true 时标记为严重安全问题 - 提示管理员配置更安全的代理信任策略 5. **安装脚本优化** - 默认配置 TRUST_PROXY=1(单层Nginx场景) - 添加详细的配置说明和安全警告 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/server.js | 123 +++++++++++++++++++++++++++++++--------------- install.sh | 10 ++++ 2 files changed, 93 insertions(+), 40 deletions(-) diff --git a/backend/server.js b/backend/server.js index 5639328..4529b43 100644 --- a/backend/server.js +++ b/backend/server.js @@ -62,8 +62,34 @@ function getSecureBaseUrl(req) { return `${getProtocol(req)}://${req.get('host')}`; } -// 在反向代理(如 Nginx/Cloudflare)后部署时,信任代理以正确识别协议/IP/HTTPS -app.set('trust proxy', process.env.TRUST_PROXY || true); +// ===== 安全配置:信任代理 ===== +// 默认不信任任何代理(直接暴露场景) +// 配置选项: +// - false: 不信任代理(默认,直接暴露) +// - true: 信任所有代理(不推荐,易被伪造) +// - 1/2/3: 信任前N跳代理(推荐,如 Nginx 后部署用 1) +// - 'loopback': 仅信任本地回环地址 +// - '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16': 信任指定IP/CIDR段 +const TRUST_PROXY_RAW = process.env.TRUST_PROXY; +let trustProxyValue = false; // 默认不信任 + +if (TRUST_PROXY_RAW !== undefined && TRUST_PROXY_RAW !== '') { + if (TRUST_PROXY_RAW === 'true') { + trustProxyValue = true; + console.warn('[安全警告] TRUST_PROXY=true 将信任所有代理,存在 IP/协议伪造风险!建议设置为具体跳数(1)或IP段'); + } else if (TRUST_PROXY_RAW === 'false') { + trustProxyValue = false; + } else if (/^\d+$/.test(TRUST_PROXY_RAW)) { + // 数字:信任前N跳 + trustProxyValue = parseInt(TRUST_PROXY_RAW, 10); + } else { + // 字符串:loopback 或 IP/CIDR 列表 + trustProxyValue = TRUST_PROXY_RAW; + } +} + +app.set('trust proxy', trustProxyValue); +console.log(`[安全] trust proxy 配置: ${JSON.stringify(trustProxyValue)}`); // 配置CORS - 严格白名单模式 const allowedOrigins = process.env.ALLOWED_ORIGINS @@ -114,10 +140,15 @@ app.use(express.json()); app.use(cookieParser()); // 强制HTTPS(可通过环境变量控制,默认关闭以兼容本地环境) +// 安全说明:使用 req.secure 判断,该值基于 trust proxy 配置, +// 只有在信任代理链中的代理才会被采信其 X-Forwarded-Proto 头 app.use((req, res, next) => { if (!ENFORCE_HTTPS) return next(); - const proto = req.get('x-forwarded-proto') || (req.secure ? 'https' : 'http'); - if (proto !== 'https') { + + // req.secure 由 Express 根据 trust proxy 配置计算: + // - 如果 trust proxy = false,仅检查直接连接是否为 TLS + // - 如果 trust proxy 已配置,会检查可信代理的 X-Forwarded-Proto + if (!req.secure) { return res.status(400).json({ success: false, message: '仅支持HTTPS访问,请使用HTTPS' @@ -150,8 +181,9 @@ app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); // XSS保护 res.setHeader('X-XSS-Protection', '1; mode=block'); - // HTTPS严格传输安全 - if (req.secure || req.headers['x-forwarded-proto'] === 'https') { + // HTTPS严格传输安全(仅在可信的 HTTPS 连接时设置) + // req.secure 基于 trust proxy 配置,不会被不可信代理伪造 + if (req.secure) { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } // 内容安全策略 @@ -317,26 +349,14 @@ app.use((req, res, next) => { next(); }); -// 获取正确的协议(考虑反向代理) +// 获取正确的协议(基于可信代理链) +// 安全说明:req.protocol 由 Express 根据 trust proxy 配置计算, +// 只有可信代理的 X-Forwarded-Proto 才会被采信 function getProtocol(req) { - // 1. 检查 X-Forwarded-Proto 头(nginx 代理传递的协议) - const forwardedProto = req.get('X-Forwarded-Proto'); - if (forwardedProto) { - return forwardedProto.split(',')[0].trim(); - } - - // 2. 检查 req.protocol - if (req.protocol) { - return req.protocol; - } - - // 3. 如果配置了SSL,默认使用https - if (req.secure) { - return 'https'; - } - - // 4. 默认使用https(因为生产环境应该都配置了SSL) - return 'https'; + // req.protocol 会根据 trust proxy 配置: + // - trust proxy = false: 仅检查直接连接(TLS -> 'https', 否则 'http') + // - trust proxy 已配置: 会检查可信代理的 X-Forwarded-Proto + return req.protocol || (req.secure ? 'https' : 'http'); } // 文件上传配置(临时存储) @@ -448,13 +468,15 @@ class RateLimiter { }, 5 * 60 * 1000); } - // 获取客户端IP(支持反向代理) + // 获取客户端IP(基于可信代理链) + // 安全说明:req.ip 由 Express 根据 trust proxy 配置计算, + // 只有可信代理的 X-Forwarded-For 才会被采信 getClientKey(req) { - const forwarded = req.get('X-Forwarded-For'); - if (forwarded) { - return forwarded.split(',')[0].trim(); - } - return req.ip || req.connection.remoteAddress || 'unknown'; + // req.ip 会根据 trust proxy 配置: + // - trust proxy = false: 使用直接连接的 IP(socket 地址) + // - trust proxy = 1: 取 X-Forwarded-For 的最后 1 个 IP + // - trust proxy = true: 取 X-Forwarded-For 的第 1 个 IP(不推荐) + return req.ip || req.socket?.remoteAddress || 'unknown'; } // 检查是否被封锁 @@ -970,7 +992,8 @@ async function sendMail(to, subject, html) { // 检查邮件发送限流 function checkMailRateLimit(req, type = 'mail') { - const clientKey = `${type}:${req.get('X-Forwarded-For') || req.ip || req.connection.remoteAddress || 'unknown'}`; + // 使用 req.ip,基于 trust proxy 配置获取可信的客户端 IP + const clientKey = `${type}:${req.ip || req.socket?.remoteAddress || 'unknown'}`; const res30 = mailLimiter30Min.recordFailure(clientKey); if (res30.blocked) { @@ -3512,16 +3535,36 @@ app.get('/api/admin/health-check', authMiddleware, adminMiddleware, async (req, suggestion: null }); - // 9. 信任代理配置(反向代理环境) + // 9. 信任代理配置(安全检查) const trustProxy = app.get('trust proxy'); + let trustProxyStatus = 'pass'; + let trustProxyMessage = ''; + let trustProxySuggestion = null; + + if (trustProxy === true) { + // trust proxy = true 是不安全的配置 + trustProxyStatus = 'fail'; + trustProxyMessage = 'trust proxy = true(信任所有代理),客户端可伪造 IP/协议!'; + trustProxySuggestion = '建议设置 TRUST_PROXY=1(单层代理)或具体的代理 IP 段'; + if (overallStatus !== 'critical') overallStatus = 'critical'; + } else if (trustProxy === false || !trustProxy) { + trustProxyStatus = 'info'; + trustProxyMessage = '未启用 trust proxy(直接暴露模式)'; + trustProxySuggestion = '如在 Nginx/CDN 后部署,需配置 TRUST_PROXY=1'; + } else if (typeof trustProxy === 'number') { + trustProxyStatus = 'pass'; + trustProxyMessage = `trust proxy = ${trustProxy}(信任前 ${trustProxy} 跳代理)`; + } else { + trustProxyStatus = 'pass'; + trustProxyMessage = `trust proxy = "${trustProxy}"(信任指定代理)`; + } + checks.push({ - name: '反向代理支持', - category: 'config', - status: trustProxy ? 'pass' : 'info', - message: trustProxy - ? '已启用trust proxy,支持X-Forwarded-For' - : '未启用trust proxy,直接暴露时无影响', - suggestion: null + name: '信任代理配置', + category: 'security', + status: trustProxyStatus, + message: trustProxyMessage, + suggestion: trustProxySuggestion }); // 10. Node环境 diff --git a/install.sh b/install.sh index 32b24c1..d669028 100644 --- a/install.sh +++ b/install.sh @@ -2180,6 +2180,16 @@ ALLOWED_ORIGINS=${ALLOWED_ORIGINS_VALUE} # HTTPS 环境必须设置为 true COOKIE_SECURE=${COOKIE_SECURE_VALUE} +# 信任代理配置(重要安全配置) +# 在 Nginx/CDN 后部署时必须配置,否则无法正确识别客户端 IP 和协议 +# 配置选项: +# - false: 不信任代理(直接暴露,默认值) +# - 1: 信任前 1 跳代理(单层 Nginx,推荐) +# - 2: 信任前 2 跳代理(CDN + Nginx) +# - loopback: 仅信任本地回环地址 +# 警告:不要设置为 true,这会信任所有代理,存在 IP/协议伪造风险! +TRUST_PROXY=1 + # 公开端口(nginx监听的端口,用于生成分享链接) # 如果使用标准端口(80/443)或未配置,分享链接将不包含端口号 PUBLIC_PORT=${HTTP_PORT}