后端安全增强: - 新增 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>
141 lines
4.3 KiB
JavaScript
141 lines
4.3 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
||
const { UserDB } = require('./database');
|
||
|
||
// JWT密钥(必须在环境变量中设置)
|
||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||
|
||
// 安全检查:验证JWT密钥配置
|
||
const DEFAULT_SECRETS = [
|
||
'your-secret-key-change-in-production',
|
||
'your-secret-key-change-in-production-PLEASE-CHANGE-THIS'
|
||
];
|
||
|
||
if (DEFAULT_SECRETS.includes(JWT_SECRET)) {
|
||
const errorMsg = `
|
||
╔═══════════════════════════════════════════════════════════════╗
|
||
║ ⚠️ 安全警告 ⚠️ ║
|
||
╠═══════════════════════════════════════════════════════════════╣
|
||
║ JWT_SECRET 使用默认值,存在严重安全风险! ║
|
||
║ ║
|
||
║ 请立即设置环境变量 JWT_SECRET ║
|
||
║ 生成随机密钥: ║
|
||
║ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||
║ ║
|
||
║ 在 backend/.env 文件中设置: ║
|
||
║ JWT_SECRET=你生成的随机密钥 ║
|
||
╚═══════════════════════════════════════════════════════════════╝
|
||
`;
|
||
|
||
if (process.env.NODE_ENV === 'production') {
|
||
console.error(errorMsg);
|
||
throw new Error('生产环境必须设置 JWT_SECRET!');
|
||
} else {
|
||
console.warn(errorMsg);
|
||
}
|
||
}
|
||
|
||
console.log('[安全] JWT密钥已配置');
|
||
|
||
// 生成JWT Token
|
||
function generateToken(user) {
|
||
return jwt.sign(
|
||
{
|
||
id: user.id,
|
||
username: user.username,
|
||
is_admin: user.is_admin
|
||
},
|
||
JWT_SECRET,
|
||
{ expiresIn: '7d' }
|
||
);
|
||
}
|
||
|
||
// 验证Token中间件
|
||
function authMiddleware(req, res, next) {
|
||
// 从请求头或HttpOnly Cookie获取token(不再接受URL参数以避免泄露)
|
||
const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token;
|
||
|
||
if (!token) {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '未提供认证令牌'
|
||
});
|
||
}
|
||
|
||
try {
|
||
const decoded = jwt.verify(token, JWT_SECRET);
|
||
const user = UserDB.findById(decoded.id);
|
||
|
||
if (!user) {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '用户不存在'
|
||
});
|
||
}
|
||
|
||
if (user.is_banned) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '账号已被封禁'
|
||
});
|
||
}
|
||
|
||
if (!user.is_active) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '账号未激活'
|
||
});
|
||
}
|
||
|
||
// 将用户信息附加到请求对象(包含所有存储相关字段)
|
||
req.user = {
|
||
id: user.id,
|
||
username: user.username,
|
||
email: user.email,
|
||
is_admin: user.is_admin,
|
||
has_ftp_config: user.has_ftp_config,
|
||
ftp_host: user.ftp_host,
|
||
ftp_port: user.ftp_port,
|
||
ftp_user: user.ftp_user,
|
||
ftp_password: user.ftp_password,
|
||
http_download_base_url: user.http_download_base_url,
|
||
// 存储相关字段(v2.0新增)
|
||
storage_permission: user.storage_permission || 'sftp_only',
|
||
current_storage_type: user.current_storage_type || 'sftp',
|
||
local_storage_quota: user.local_storage_quota || 1073741824,
|
||
local_storage_used: user.local_storage_used || 0
|
||
};
|
||
|
||
next();
|
||
} catch (error) {
|
||
if (error.name === 'TokenExpiredError') {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '令牌已过期'
|
||
});
|
||
}
|
||
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '无效的令牌'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 管理员权限中间件
|
||
function adminMiddleware(req, res, next) {
|
||
if (!req.user || !req.user.is_admin) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '需要管理员权限'
|
||
});
|
||
}
|
||
next();
|
||
}
|
||
|
||
module.exports = {
|
||
JWT_SECRET,
|
||
generateToken,
|
||
authMiddleware,
|
||
adminMiddleware
|
||
};
|