🔒 增强安全防护:防止 SSRF 攻击和 Token 泄露
后端安全增强: - 新增 SSRF 防护机制,验证 SFTP 目标地址 - 添加 isPrivateIp() 函数,检测并阻止连接内网地址 - 添加 validateSftpDestination() 函数,验证主机名和端口 - 支持 DNS 解析和 IP 地址验证 - 添加 SFTP 连接超时配置(默认8秒) - 移除 URL 参数中的 token 认证,只接受 Header 或 HttpOnly Cookie 前端安全改进: - 移除下载链接中的 token 参数 - 改为依赖同域 Cookie 进行身份验证 - 避免 token 在 URL 中暴露,防止日志泄露 环境变量配置: - ALLOW_PRIVATE_SFTP=true 可允许连接内网(测试环境) - SFTP_CONNECT_TIMEOUT 可配置连接超时时间 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,8 @@ function generateToken(user) {
|
||||
|
||||
// 验证Token中间件
|
||||
function authMiddleware(req, res, next) {
|
||||
// 从请求头、cookie或URL参数中获取token
|
||||
const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token || req.query?.token;
|
||||
// 从请求头或HttpOnly Cookie获取token(不再接受URL参数以避免泄露)
|
||||
const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token;
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
|
||||
@@ -14,6 +14,8 @@ const fs = require('fs');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const archiver = require('archiver');
|
||||
const { exec, execSync } = require('child_process');
|
||||
const net = require('net');
|
||||
const dns = require('dns').promises;
|
||||
const util = require('util');
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
@@ -626,14 +628,70 @@ function cleanupOldTempFiles() {
|
||||
console.error('[清理] 清理临时文件目录失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function isPrivateIp(ip) {
|
||||
if (!net.isIP(ip)) return false;
|
||||
return ip.startsWith('10.') ||
|
||||
ip.startsWith('192.168.') ||
|
||||
ip.startsWith('172.16.') ||
|
||||
ip.startsWith('172.17.') ||
|
||||
ip.startsWith('172.18.') ||
|
||||
ip.startsWith('172.19.') ||
|
||||
ip.startsWith('172.20.') ||
|
||||
ip.startsWith('172.21.') ||
|
||||
ip.startsWith('172.22.') ||
|
||||
ip.startsWith('172.23.') ||
|
||||
ip.startsWith('172.24.') ||
|
||||
ip.startsWith('172.25.') ||
|
||||
ip.startsWith('172.26.') ||
|
||||
ip.startsWith('172.27.') ||
|
||||
ip.startsWith('172.28.') ||
|
||||
ip.startsWith('172.29.') ||
|
||||
ip.startsWith('172.30.') ||
|
||||
ip.startsWith('172.31.') ||
|
||||
ip.startsWith('127.') ||
|
||||
ip === '0.0.0.0' ||
|
||||
ip === '::1' ||
|
||||
ip.startsWith('fe80:') ||
|
||||
ip.startsWith('fd');
|
||||
}
|
||||
|
||||
async function validateSftpDestination(host, port) {
|
||||
if (!host || typeof host !== 'string' || host.length > 255) {
|
||||
throw new Error('无效的SFTP主机');
|
||||
}
|
||||
if (host.includes('://') || host.includes('/')) {
|
||||
throw new Error('SFTP主机不能包含协议或路径');
|
||||
}
|
||||
|
||||
const portNum = parseInt(port, 10) || 22;
|
||||
if (portNum < 1 || portNum > 65535) {
|
||||
throw new Error('SFTP端口范围应为1-65535');
|
||||
}
|
||||
|
||||
let resolvedIp;
|
||||
try {
|
||||
const lookup = await dns.lookup(host);
|
||||
resolvedIp = lookup.address;
|
||||
} catch (err) {
|
||||
throw new Error('无法解析SFTP主机,请检查域名或IP');
|
||||
}
|
||||
|
||||
if (isPrivateIp(resolvedIp) && process.env.ALLOW_PRIVATE_SFTP !== 'true') {
|
||||
throw new Error('出于安全考虑,不允许连接内网地址');
|
||||
}
|
||||
|
||||
return { host, port: portNum, resolvedIp };
|
||||
}
|
||||
// SFTP连接
|
||||
async function connectToSFTP(config) {
|
||||
const sftp = new SftpClient();
|
||||
await sftp.connect({
|
||||
host: config.ftp_host,
|
||||
port: config.ftp_port || 22,
|
||||
port: parseInt(config.ftp_port, 10) || 22,
|
||||
username: config.ftp_user,
|
||||
password: config.ftp_password
|
||||
password: config.ftp_password,
|
||||
readyTimeout: parseInt(process.env.SFTP_CONNECT_TIMEOUT || '8000', 10)
|
||||
});
|
||||
return sftp;
|
||||
}
|
||||
@@ -1215,7 +1273,7 @@ app.post('/api/user/update-ftp',
|
||||
|
||||
try {
|
||||
const { ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url } = req.body;
|
||||
// 调试日志:查看接收到的配置
|
||||
// 调试日志:查看接收到的配置(掩码密码)
|
||||
console.log("[DEBUG] 收到SFTP配置:", {
|
||||
ftp_host,
|
||||
ftp_port,
|
||||
@@ -1224,7 +1282,6 @@ app.post('/api/user/update-ftp',
|
||||
http_download_base_url
|
||||
});
|
||||
|
||||
|
||||
// 如果用户已配置FTP且密码为空,使用现有密码
|
||||
let actualPassword = ftp_password;
|
||||
if (!ftp_password && req.user.has_ftp_config && req.user.ftp_password) {
|
||||
@@ -1236,9 +1293,12 @@ app.post('/api/user/update-ftp',
|
||||
});
|
||||
}
|
||||
|
||||
// 主机校验与防SSRF
|
||||
const { port: safePort } = await validateSftpDestination(ftp_host, ftp_port);
|
||||
|
||||
// 验证FTP连接
|
||||
try {
|
||||
const sftp = await connectToSFTP({ ftp_host, ftp_port, ftp_user, ftp_password: actualPassword });
|
||||
const sftp = await connectToSFTP({ ftp_host, ftp_port: safePort, ftp_user, ftp_password: actualPassword });
|
||||
await sftp.end();
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
@@ -1250,7 +1310,7 @@ app.post('/api/user/update-ftp',
|
||||
// 更新用户配置
|
||||
UserDB.update(req.user.id, {
|
||||
ftp_host,
|
||||
ftp_port,
|
||||
ftp_port: safePort,
|
||||
ftp_user,
|
||||
ftp_password: actualPassword,
|
||||
http_download_base_url: http_download_base_url || null,
|
||||
|
||||
Reference in New Issue
Block a user