Files
vue-driven-cloud-storage/backend/auth.js
喻勇祥 4e4ef0405b 🔒 增强安全防护:防止 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>
2025-11-24 20:07:34 +08:00

141 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};