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:
@@ -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环境
|
||||
|
||||
10
install.sh
10
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}
|
||||
|
||||
Reference in New Issue
Block a user