fix(security): 修复信任代理和HTTPS检测的安全漏洞

## 问题修复

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-27 19:42:25 +08:00
parent 1dde17bb04
commit 15ea15518c
2 changed files with 93 additions and 40 deletions

View File

@@ -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: 使用直接连接的 IPsocket 地址)
// - 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环境

View File

@@ -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}