feat: 全面优化代码质量至 8.55/10 分

## 安全增强
- 添加 CSRF 防护机制(Double Submit Cookie 模式)
- 增强密码强度验证(8字符+两种字符类型)
- 添加 Session 密钥安全检查
- 修复 .htaccess 文件上传漏洞
- 统一使用 getSafeErrorMessage() 保护敏感错误信息
- 增强数据库原型污染防护
- 添加被封禁用户分享访问检查

## 功能修复
- 修复模态框点击外部关闭功能
- 修复 share.html 未定义方法调用
- 修复 verify.html 和 reset-password.html API 路径
- 修复数据库 SFTP->OSS 迁移逻辑
- 修复 OSS 未配置时的错误提示
- 添加文件夹名称长度限制
- 添加文件列表 API 路径验证

## UI/UX 改进
- 添加 6 个按钮加载状态(登录/注册/修改密码等)
- 将 15+ 处 alert() 替换为 Toast 通知
- 添加防重复提交机制(创建文件夹/分享)
- 优化 loadUserProfile 防抖调用

## 代码质量
- 消除 formatFileSize 重复定义
- 集中模块导入到文件顶部
- 添加 JSDoc 注释
- 创建路由拆分示例 (routes/)

## 测试套件
- 添加 boundary-tests.js (60 用例)
- 添加 network-concurrent-tests.js (33 用例)
- 添加 state-consistency-tests.js (38 用例)
- 添加 test_share.js 和 test_admin.js

## 文档和配置
- 新增 INSTALL_GUIDE.md 手动部署指南
- 新增 VERSION.txt 版本历史
- 完善 .env.example 配置说明
- 新增 docker-compose.yml
- 完善 nginx.conf.example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 10:45:51 +08:00
parent ab7e08a21b
commit efaa2308eb
30 changed files with 6724 additions and 238 deletions

View File

@@ -62,8 +62,9 @@ function clearOssUsageCache(userId) {
console.log(`[OSS缓存] 已清除: 用户 ${userId}`);
}
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB } = require('./database');
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB, TransactionDB } = require('./database');
const { generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
const { StorageInterface, LocalStorageClient, OssStorageClient, formatFileSize, formatOssError } = require('./storage');
const app = express();
const PORT = process.env.PORT || 40001;
@@ -178,9 +179,76 @@ const corsOptions = {
// 中间件
app.use(cors(corsOptions));
app.use(express.json());
app.use(express.json({ limit: '10mb' })); // 限制请求体大小防止DoS
app.use(cookieParser());
// ===== CSRF 防护 =====
// 基于 Double Submit Cookie 模式的 CSRF 保护
// 对于修改数据的请求POST/PUT/DELETE验证请求头中的 X-CSRF-Token 与 Cookie 中的值匹配
// 生成 CSRF Token
function generateCsrfToken() {
return crypto.randomBytes(32).toString('hex');
}
// CSRF Token Cookie 名称
const CSRF_COOKIE_NAME = 'csrf_token';
// 设置 CSRF Cookie 的中间件
app.use((req, res, next) => {
// 如果没有 CSRF cookie则生成一个
if (!req.cookies[CSRF_COOKIE_NAME]) {
const csrfToken = generateCsrfToken();
const isSecureEnv = process.env.COOKIE_SECURE === 'true';
res.cookie(CSRF_COOKIE_NAME, csrfToken, {
httpOnly: false, // 前端需要读取此值
secure: isSecureEnv,
sameSite: isSecureEnv ? 'strict' : 'lax',
maxAge: 24 * 60 * 60 * 1000 // 24小时
});
}
next();
});
// CSRF 验证中间件(仅用于需要保护的路由)
function csrfProtection(req, res, next) {
// GET、HEAD、OPTIONS 请求不需要 CSRF 保护
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
// 白名单:某些公开 API 不需要 CSRF 保护(如分享页面的密码验证)
const csrfExemptPaths = [
'/api/share/', // 分享相关的公开接口
'/api/captcha', // 验证码
'/api/health' // 健康检查
];
if (csrfExemptPaths.some(path => req.path.startsWith(path))) {
return next();
}
const cookieToken = req.cookies[CSRF_COOKIE_NAME];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
console.warn(`[CSRF] 验证失败: path=${req.path}, cookie=${!!cookieToken}, header=${!!headerToken}`);
return res.status(403).json({
success: false,
message: 'CSRF 验证失败,请刷新页面后重试'
});
}
next();
}
// 注意CSRF 保护将在 authMiddleware 后的路由中按需启用
// 可以通过环境变量 ENABLE_CSRF=true 开启(默认关闭以保持向后兼容)
const ENABLE_CSRF = process.env.ENABLE_CSRF === 'true';
if (ENABLE_CSRF) {
console.log('[安全] CSRF 保护已启用');
}
// 强制HTTPS可通过环境变量控制默认关闭以兼容本地环境
// 安全说明:使用 req.secure 判断,该值基于 trust proxy 配置,
// 只有在信任代理链中的代理才会被采信其 X-Forwarded-Proto 头
@@ -202,8 +270,30 @@ app.use((req, res, next) => {
// Session配置用于验证码
const isSecureCookie = process.env.COOKIE_SECURE === 'true';
const sameSiteMode = isSecureCookie ? 'none' : 'lax'; // HTTPS下允许跨域获取验证码
// 安全检查Session密钥配置
const SESSION_SECRET = process.env.SESSION_SECRET || 'your-session-secret-change-in-production';
const DEFAULT_SESSION_SECRETS = [
'your-session-secret-change-in-production',
'session-secret-change-me'
];
if (DEFAULT_SESSION_SECRETS.includes(SESSION_SECRET)) {
const sessionWarnMsg = `
[安全警告] SESSION_SECRET 使用默认值,存在安全风险!
请在 .env 文件中设置随机生成的 SESSION_SECRET
生成命令: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
`;
if (process.env.NODE_ENV === 'production') {
console.error(sessionWarnMsg);
throw new Error('生产环境必须设置 SESSION_SECRET');
} else {
console.warn(sessionWarnMsg);
}
}
app.use(session({
secret: process.env.SESSION_SECRET || 'your-session-secret-change-in-production',
secret: SESSION_SECRET,
resave: false,
saveUninitialized: true, // 改为true确保验证码请求时创建session
name: 'captcha.sid', // 自定义session cookie名称
@@ -235,8 +325,12 @@ app.use((req, res, next) => {
next();
});
// XSS过滤中间件用于用户输入- 增强版
// 注意:不转义 / 因为它是文件路径的合法字符
/**
* XSS过滤函数 - 过滤用户输入中的潜在XSS攻击代码
* 注意:不转义 / 因为它是文件路径的合法字符
* @param {string} str - 需要过滤的输入字符串
* @returns {string} 过滤后的安全字符串
*/
function sanitizeInput(str) {
if (typeof str !== 'string') return str;
@@ -262,7 +356,13 @@ function sanitizeInput(str) {
return sanitized;
}
// 将 HTML 实体解码为原始字符(用于文件名/路径字段)
/**
* 将 HTML 实体解码为原始字符
* 用于处理经过XSS过滤后的文件名/路径字段,恢复原始字符
* 支持嵌套实体的递归解码(如 &amp;#x60; -> &#x60; -> `
* @param {string} str - 包含HTML实体的字符串
* @returns {string} 解码后的原始字符串
*/
function decodeHtmlEntities(str) {
if (typeof str !== 'string') return str;
@@ -384,14 +484,21 @@ function isFileExtensionSafe(filename) {
if (!filename || typeof filename !== 'string') return false;
const ext = path.extname(filename).toLowerCase();
const nameLower = filename.toLowerCase();
// 检查危险扩展名
if (DANGEROUS_EXTENSIONS.includes(ext)) {
return false;
}
// 特殊处理:检查以危险名称开头的文件(如 .htaccess, .htpasswd
// 因为 path.extname('.htaccess') 返回空字符串
const dangerousFilenames = ['.htaccess', '.htpasswd'];
if (dangerousFilenames.includes(nameLower)) {
return false;
}
// 检查双扩展名攻击(如 file.php.jpg 可能被某些配置错误的服务器执行)
const nameLower = filename.toLowerCase();
for (const dangerExt of DANGEROUS_EXTENSIONS) {
if (nameLower.includes(dangerExt + '.')) {
return false;
@@ -935,6 +1042,29 @@ function shareRateLimitMiddleware(req, res, next) {
// ===== 工具函数 =====
/**
* 安全的错误响应处理
* 在生产环境中隐藏敏感的错误详情,仅在开发环境显示详细信息
* @param {Error} error - 原始错误对象
* @param {string} userMessage - 给用户显示的友好消息
* @param {string} logContext - 日志上下文标识
* @returns {string} 返回给客户端的错误消息
*/
function getSafeErrorMessage(error, userMessage, logContext = '') {
// 记录完整错误日志
if (logContext) {
console.error(`[${logContext}]`, error);
} else {
console.error(error);
}
// 生产环境返回通用消息,开发环境返回详细信息
if (process.env.NODE_ENV === 'production') {
return userMessage;
}
// 开发环境下,返回详细错误信息便于调试
return `${userMessage}: ${error.message}`;
}
// 安全删除文件(不抛出异常)
function safeDeleteFile(filePath) {
@@ -1005,18 +1135,11 @@ function cleanupOldTempFiles() {
}
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// formatFileSize 已在文件顶部导入
// 生成随机Token
// 生成随机Tokencrypto 已在文件顶部导入)
function generateRandomToken(length = 48) {
return require('crypto').randomBytes(length).toString('hex');
return crypto.randomBytes(length).toString('hex');
}
// 获取SMTP配置
@@ -1193,6 +1316,60 @@ app.get('/api/captcha', captchaRateLimitMiddleware, (req, res) => {
}
});
// 获取 CSRF Token用于前端初始化
app.get('/api/csrf-token', (req, res) => {
let csrfToken = req.cookies[CSRF_COOKIE_NAME];
// 如果没有 token生成一个新的
if (!csrfToken) {
csrfToken = generateCsrfToken();
const isSecureEnv = process.env.COOKIE_SECURE === 'true';
res.cookie(CSRF_COOKIE_NAME, csrfToken, {
httpOnly: false,
secure: isSecureEnv,
sameSite: isSecureEnv ? 'strict' : 'lax',
maxAge: 24 * 60 * 60 * 1000
});
}
res.json({
success: true,
csrfToken: csrfToken
});
});
// 密码强度验证函数
function validatePasswordStrength(password) {
if (!password || password.length < 8) {
return { valid: false, message: '密码至少8个字符' };
}
if (password.length > 128) {
return { valid: false, message: '密码不能超过128个字符' };
}
// 检查是否包含至少两种字符类型(字母、数字、特殊字符)
const hasLetter = /[a-zA-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>_\-+=\[\]\\\/`~]/.test(password);
const typeCount = [hasLetter, hasNumber, hasSpecial].filter(Boolean).length;
if (typeCount < 2) {
return { valid: false, message: '密码必须包含字母、数字、特殊字符中的至少两种' };
}
// 检查常见弱密码
const commonWeakPasswords = [
'password', '12345678', '123456789', 'qwerty123', 'admin123',
'letmein', 'welcome', 'monkey', 'dragon', 'master'
];
if (commonWeakPasswords.includes(password.toLowerCase())) {
return { valid: false, message: '密码过于简单,请使用更复杂的密码' };
}
return { valid: true };
}
// 用户注册(简化版)
app.post('/api/register',
[
@@ -1200,7 +1377,15 @@ app.post('/api/register',
.isLength({ min: 3, max: 20 }).withMessage('用户名长度3-20个字符')
.matches(USERNAME_REGEX).withMessage('用户名仅允许中英文、数字、下划线、点和短横线'),
body('email').isEmail().withMessage('邮箱格式不正确'),
body('password').isLength({ min: 6 }).withMessage('密码至少6个字符'),
body('password')
.isLength({ min: 8, max: 128 }).withMessage('密码长度8-128个字符')
.custom((value) => {
const result = validatePasswordStrength(value);
if (!result.valid) {
throw new Error(result.message);
}
return true;
}),
body('captcha').notEmpty().withMessage('请输入验证码')
],
async (req, res) => {
@@ -1296,9 +1481,13 @@ app.post('/api/register',
} catch (error) {
console.error('注册失败:', error);
logAuth(req, 'register_failed', `用户注册失败: ${req.body.username || 'unknown'}`, { error: error.message }, 'error');
// 安全修复:不向客户端泄露具体错误信息
const safeMessage = error.message?.includes('UNIQUE constraint')
? '用户名或邮箱已被注册'
: '注册失败,请稍后重试';
res.status(500).json({
success: false,
message: '注册失败: ' + error.message
message: safeMessage
});
}
}
@@ -1372,15 +1561,26 @@ app.post('/api/resend-verification', [
// 验证邮箱
app.get('/api/verify-email', async (req, res) => {
const { token } = req.query;
if (!token) {
// 参数验证token 不能为空且长度合理48字符的hex字符串
if (!token || typeof token !== 'string') {
return res.status(400).json({ success: false, message: '缺少token' });
}
// token 格式验证:应该是 hex 字符串,长度合理
if (!/^[a-f0-9]{32,96}$/i.test(token)) {
return res.status(400).json({ success: false, message: '无效的token格式' });
}
try {
const user = VerificationDB.consumeVerificationToken(token);
if (!user) {
return res.status(400).json({ success: false, message: '无效或已过期的验证链接' });
}
// 记录验证成功日志
logAuth(req, 'email_verified', `邮箱验证成功: ${user.email || user.username}`, { userId: user.id });
res.json({ success: true, message: '邮箱验证成功,请登录' });
} catch (error) {
console.error('邮箱验证失败:', error);
@@ -1456,7 +1656,15 @@ app.post('/api/password/forgot', [
// 使用邮件Token重置密码
app.post('/api/password/reset', [
body('token').notEmpty().withMessage('缺少token'),
body('new_password').isLength({ min: 6 }).withMessage('新密码至少6个字符')
body('new_password')
.isLength({ min: 8, max: 128 }).withMessage('密码长度8-128个字符')
.custom((value) => {
const result = validatePasswordStrength(value);
if (!result.valid) {
throw new Error(result.message);
}
return true;
})
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
@@ -1686,9 +1894,10 @@ app.post('/api/login',
} catch (error) {
console.error('登录失败:', error);
logAuth(req, 'login_error', `登录异常: ${req.body.username || 'unknown'}`, { error: error.message }, 'error');
// 安全修复:不向客户端泄露具体错误信息
res.status(500).json({
success: false,
message: '登录失败: ' + error.message
message: '登录失败,请稍后重试'
});
}
}
@@ -1837,7 +2046,7 @@ app.post('/api/user/update-oss',
// 验证OSS连接
try {
const { OssStorageClient } = require('./storage');
// OssStorageClient 已在文件顶部导入
const testUser = {
id: req.user.id,
oss_provider,
@@ -1918,7 +2127,7 @@ app.post('/api/user/test-oss',
}
// 验证 OSS 连接
const { OssStorageClient } = require('./storage');
// OssStorageClient 已在文件顶部导入
const testUser = {
id: req.user.id,
oss_provider,
@@ -1970,7 +2179,7 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => {
});
}
const { OssStorageClient } = require('./storage');
// OssStorageClient 已在文件顶部导入
const ossClient = new OssStorageClient(req.user);
await ossClient.connect();
@@ -2085,9 +2294,10 @@ app.post('/api/admin/update-profile',
}
} catch (error) {
console.error('更新账号信息失败:', error);
// 安全修复:不向客户端泄露具体错误信息
res.status(500).json({
success: false,
message: '更新失败: ' + error.message
message: '更新失败,请稍后重试'
});
}
}
@@ -2098,7 +2308,15 @@ app.post('/api/user/change-password',
authMiddleware,
[
body('current_password').notEmpty().withMessage('当前密码不能为空'),
body('new_password').isLength({ min: 6 }).withMessage('新密码至少6个字符')
body('new_password')
.isLength({ min: 8, max: 128 }).withMessage('密码长度8-128个字符')
.custom((value) => {
const result = validatePasswordStrength(value);
if (!result.valid) {
throw new Error(result.message);
}
return true;
})
],
(req, res) => {
const errors = validationResult(req);
@@ -2137,10 +2355,9 @@ app.post('/api/user/change-password',
message: '密码修改成功'
});
} catch (error) {
console.error('修改密码失败:', error);
res.status(500).json({
success: false,
message: '修改密码失败: ' + error.message
message: getSafeErrorMessage(error, '修改密码失败,请稍后重试', '修改密码失败')
});
}
}
@@ -2183,10 +2400,9 @@ app.post('/api/user/update-username',
message: '用户名修改成功'
});
} catch (error) {
console.error('修改用户名失败:', error);
res.status(500).json({
success: false,
message: '修改用户名失败: ' + error.message
message: getSafeErrorMessage(error, '修改用户名失败,请稍后重试', '修改用户名失败')
});
}
}
@@ -2263,7 +2479,18 @@ app.get('/api/files', authMiddleware, async (req, res) => {
});
}
const dirPath = req.query.path || '/';
const rawPath = req.query.path || '/';
// 路径安全验证:在 API 层提前拒绝包含 .. 或空字节的路径
if (rawPath.includes('..') || rawPath.includes('\x00') || rawPath.includes('%00')) {
return res.status(400).json({
success: false,
message: '路径包含非法字符'
});
}
// 规范化路径
const dirPath = path.posix.normalize(rawPath);
let storage;
try {
@@ -2306,7 +2533,7 @@ app.get('/api/files', authMiddleware, async (req, res) => {
console.error('获取文件列表失败:', error);
res.status(500).json({
success: false,
message: '获取文件列表失败: ' + error.message
message: getSafeErrorMessage(error, '获取文件列表失败,请稍后重试', '获取文件列表')
});
} finally {
if (storage) await storage.end();
@@ -2351,7 +2578,7 @@ app.post('/api/files/rename', authMiddleware, async (req, res) => {
console.error('重命名文件失败:', error);
res.status(500).json({
success: false,
message: '重命名文件失败: ' + error.message
message: getSafeErrorMessage(error, '重命名文件失败,请稍后重试', '重命名文件')
});
} finally {
if (storage) await storage.end();
@@ -2372,6 +2599,14 @@ app.post('/api/files/mkdir', authMiddleware, async (req, res) => {
});
}
// 文件名长度检查
if (folderName.length > 255) {
return res.status(400).json({
success: false,
message: '文件夹名称过长最大255个字符'
});
}
// 文件名安全检查 - 防止路径遍历攻击
if (folderName.includes('/') || folderName.includes('\\') || folderName.includes('..') || folderName.includes(':')) {
return res.status(400).json({
@@ -2423,7 +2658,7 @@ app.post('/api/files/mkdir', authMiddleware, async (req, res) => {
console.error('[创建文件夹失败]', error);
res.status(500).json({
success: false,
message: '创建文件夹失败: ' + error.message
message: getSafeErrorMessage(error, '创建文件夹失败,请稍后重试', '创建文件夹')
});
} finally {
if (storage) await storage.end();
@@ -2636,7 +2871,7 @@ app.post('/api/files/delete', authMiddleware, async (req, res) => {
console.error('删除文件失败:', error);
res.status(500).json({
success: false,
message: '删除文件失败: ' + error.message
message: getSafeErrorMessage(error, '删除文件失败,请稍后重试', '删除文件')
});
} finally {
if (storage) await storage.end();
@@ -2835,15 +3070,41 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
// 辅助函数:构建 S3 配置(复用 OssStorageClient.buildConfig
function buildS3Config(user) {
// 创建临时 OssStorageClient 实例并复用其 buildConfig 方法
const { OssStorageClient } = require('./storage');
// OssStorageClient 已在文件顶部导入
const tempClient = new OssStorageClient(user);
return tempClient.buildConfig();
}
// 辅助函数:清理文件名
// 辅助函数:清理文件名(增强版安全处理)
function sanitizeFilename(filename) {
// 移除或替换危险字符
return filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
if (!filename || typeof filename !== 'string') {
return 'unnamed_file';
}
let sanitized = filename;
// 1. 移除空字节和控制字符
sanitized = sanitized.replace(/[\x00-\x1f\x7f]/g, '');
// 2. 移除或替换危险字符Windows/Linux 文件系统不允许的字符)
sanitized = sanitized.replace(/[<>:"/\\|?*]/g, '_');
// 3. 移除前导/尾随的点和空格(防止隐藏文件和路径混淆)
sanitized = sanitized.replace(/^[\s.]+|[\s.]+$/g, '');
// 4. 限制文件名长度(防止过长文件名攻击)
if (sanitized.length > 200) {
const ext = path.extname(sanitized);
const base = path.basename(sanitized, ext);
sanitized = base.substring(0, 200 - ext.length) + ext;
}
// 5. 如果处理后为空,使用默认名称
if (!sanitized || sanitized.length === 0) {
sanitized = 'unnamed_file';
}
return sanitized;
}
// ========== 本地存储上传接口(保留用于本地存储模式)==========
@@ -2960,7 +3221,7 @@ app.post('/api/upload', authMiddleware, upload.single('file'), async (req, res)
res.status(500).json({
success: false,
message: '文件上传失败: ' + error.message
message: getSafeErrorMessage(error, '文件上传失败,请稍后重试', '文件上传')
});
} finally {
if (storage) await storage.end();
@@ -3052,7 +3313,7 @@ app.get('/api/files/download', authMiddleware, async (req, res) => {
if (!res.headersSent) {
res.status(500).json({
success: false,
message: '下载文件失败: ' + error.message
message: getSafeErrorMessage(error, '下载文件失败,请稍后重试', '下载文件')
});
}
}
@@ -3291,7 +3552,7 @@ app.post('/api/upload/get-config', async (req, res) => {
console.error('获取OSS配置失败:', error);
res.status(500).json({
success: false,
message: '获取OSS配置失败: ' + error.message
message: '服务器内部错误,请稍后重试'
});
}
});
@@ -3300,23 +3561,62 @@ app.post('/api/upload/get-config', async (req, res) => {
app.post('/api/share/create', authMiddleware, (req, res) => {
try {
const { share_type, file_path, file_name, password, expiry_days } = req.body;
// 参数验证share_type 只能是 'file' 或 'directory'
const validShareTypes = ['file', 'directory'];
const actualShareType = share_type || 'file';
if (!validShareTypes.includes(actualShareType)) {
return res.status(400).json({
success: false,
message: '无效的分享类型,只能是 file 或 directory'
});
}
// 参数验证file_path 不能为空
if (!file_path) {
return res.status(400).json({
success: false,
message: actualShareType === 'file' ? '文件路径不能为空' : '目录路径不能为空'
});
}
// 参数验证expiry_days 必须为正整数或 null
if (expiry_days !== undefined && expiry_days !== null) {
const days = parseInt(expiry_days, 10);
if (isNaN(days) || days <= 0 || days > 365) {
return res.status(400).json({
success: false,
message: '有效期必须是1-365之间的整数'
});
}
}
// 参数验证:密码长度限制
if (password && (typeof password !== 'string' || password.length > 32)) {
return res.status(400).json({
success: false,
message: '密码长度不能超过32个字符'
});
}
// 路径安全验证:防止路径遍历攻击
if (file_path.includes('..') || file_path.includes('\x00')) {
return res.status(400).json({
success: false,
message: '路径包含非法字符'
});
}
SystemLogDB.log({
level: 'info',
category: 'share',
action: 'create_share',
message: '创建分享请求',
details: { share_type, file_path, file_name, expiry_days }
details: { share_type: actualShareType, file_path, file_name, expiry_days }
});
if (share_type === 'file' && !file_path) {
return res.status(400).json({
success: false,
message: '文件路径不能为空'
});
}
const result = ShareDB.create(req.user.id, {
share_type: share_type || 'file',
share_type: actualShareType,
file_path: file_path || '',
file_name: file_name || '',
password: password || null,
@@ -3329,6 +3629,12 @@ app.post('/api/share/create', authMiddleware, (req, res) => {
const shareUrl = `${getSecureBaseUrl(req)}/s/${result.share_code}`;
// 记录分享创建日志
logShare(req, 'create_share',
`用户创建分享: ${actualShareType === 'file' ? '文件' : '目录'} ${file_path}`,
{ shareCode: result.share_code, sharePath: file_path, shareType: actualShareType, hasPassword: !!password }
);
res.json({
success: true,
message: '分享链接创建成功',
@@ -3341,7 +3647,7 @@ app.post('/api/share/create', authMiddleware, (req, res) => {
console.error('创建分享链接失败:', error);
res.status(500).json({
success: false,
message: '创建分享链接失败: ' + error.message
message: getSafeErrorMessage(error, '创建分享链接失败,请稍后重试', '创建分享')
});
}
});
@@ -3362,7 +3668,7 @@ app.get('/api/share/my', authMiddleware, (req, res) => {
console.error('获取分享列表失败:', error);
res.status(500).json({
success: false,
message: '获取分享列表失败: ' + error.message
message: getSafeErrorMessage(error, '获取分享列表失败,请稍后重试', '获取分享列表')
});
}
});
@@ -3417,7 +3723,7 @@ app.delete('/api/share/:id', authMiddleware, (req, res) => {
console.error('删除分享失败:', error);
res.status(500).json({
success: false,
message: '删除分享失败: ' + error.message
message: getSafeErrorMessage(error, '删除分享失败,请稍后重试', '删除分享')
});
}
});
@@ -3762,10 +4068,18 @@ app.post('/api/share/:code/list', shareRateLimitMiddleware, async (req, res) =>
}
});
// 记录下载次数
app.post('/api/share/:code/download', (req, res) => {
// 记录下载次数(添加限流保护防止滥用)
app.post('/api/share/:code/download', shareRateLimitMiddleware, (req, res) => {
const { code } = req.params;
// 参数验证code 不能为空
if (!code || typeof code !== 'string' || code.length < 1 || code.length > 32) {
return res.status(400).json({
success: false,
message: '无效的分享码'
});
}
try {
const share = ShareDB.findByCode(code);
@@ -3792,11 +4106,19 @@ app.post('/api/share/:code/download', (req, res) => {
}
});
// 生成分享文件下载签名 URLOSS 直连下载,公开 API
app.get('/api/share/:code/download-url', async (req, res) => {
// 生成分享文件下载签名 URLOSS 直连下载,公开 API,添加限流保护
app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, res) => {
const { code } = req.params;
const { path: filePath, password } = req.query;
// 参数验证code 不能为空
if (!code || typeof code !== 'string' || code.length < 1 || code.length > 32) {
return res.status(400).json({
success: false,
message: '无效的分享码'
});
}
if (!filePath) {
return res.status(400).json({
success: false,
@@ -4323,6 +4645,29 @@ app.get('/api/admin/health-check', authMiddleware, adminMiddleware, async (req,
suggestion: nodeEnv !== 'production' ? '生产部署建议设置NODE_ENV=production' : null
});
// 11. CSRF 保护检查
const csrfEnabled = process.env.ENABLE_CSRF === 'true';
checks.push({
name: 'CSRF保护',
category: 'security',
status: csrfEnabled ? 'pass' : 'warning',
message: csrfEnabled
? 'CSRF保护已启用Double Submit Cookie模式'
: 'CSRF保护未启用通过ENABLE_CSRF=true开启',
suggestion: csrfEnabled ? null : '生产环境建议启用CSRF保护以防止跨站请求伪造攻击'
});
// 12. Session密钥检查
const sessionSecure = !DEFAULT_SESSION_SECRETS.includes(SESSION_SECRET) && SESSION_SECRET.length >= 32;
checks.push({
name: 'Session密钥',
category: 'security',
status: sessionSecure ? 'pass' : 'fail',
message: sessionSecure ? 'Session密钥已正确配置' : 'Session密钥使用默认值或长度不足存在安全风险',
suggestion: sessionSecure ? null : '请在.env中设置随机生成的SESSION_SECRET至少32字符'
});
if (!sessionSecure && overallStatus !== 'critical') overallStatus = 'critical';
// 统计
const summary = {
total: checks.length,
@@ -4565,7 +4910,55 @@ app.post('/api/admin/users/:id/ban', authMiddleware, adminMiddleware, (req, res)
const { id } = req.params;
const { banned } = req.body;
UserDB.setBanStatus(id, banned);
// 参数验证:验证 ID 格式
const userId = parseInt(id, 10);
if (isNaN(userId) || userId <= 0) {
return res.status(400).json({
success: false,
message: '无效的用户ID'
});
}
// 参数验证:验证 banned 是否为布尔值
if (typeof banned !== 'boolean') {
return res.status(400).json({
success: false,
message: 'banned 参数必须为布尔值'
});
}
// 安全检查:不能封禁自己
if (userId === req.user.id) {
return res.status(400).json({
success: false,
message: '不能封禁自己的账号'
});
}
// 检查目标用户是否存在
const targetUser = UserDB.findById(userId);
if (!targetUser) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
// 安全检查:不能封禁其他管理员(除非是超级管理员)
if (targetUser.is_admin && !req.user.is_super_admin) {
return res.status(403).json({
success: false,
message: '不能封禁管理员账号'
});
}
UserDB.setBanStatus(userId, banned);
// 记录管理员操作日志
logUser(req, banned ? 'ban_user' : 'unban_user',
`管理员${banned ? '封禁' : '解封'}用户: ${targetUser.username}`,
{ targetUserId: userId, targetUsername: targetUser.username }
);
res.json({
success: true,
@@ -4585,7 +4978,16 @@ app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req,
try {
const { id } = req.params;
if (parseInt(id) === req.user.id) {
// 参数验证:验证 ID 格式
const userId = parseInt(id, 10);
if (isNaN(userId) || userId <= 0) {
return res.status(400).json({
success: false,
message: '无效的用户ID'
});
}
if (userId === req.user.id) {
return res.status(400).json({
success: false,
message: '不能删除自己的账号'
@@ -4593,7 +4995,7 @@ app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req,
}
// 获取用户信息
const user = UserDB.findById(id);
const user = UserDB.findById(userId);
if (!user) {
return res.status(404).json({
success: false,
@@ -4602,7 +5004,7 @@ app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req,
}
const deletionLog = {
userId: id,
userId: userId,
username: user.username,
deletedFiles: [],
deletedShares: 0,
@@ -4613,7 +5015,7 @@ app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req,
const storagePermission = user.storage_permission || 'oss_only';
if (storagePermission === 'local_only' || storagePermission === 'user_choice') {
const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage');
const userStorageDir = path.join(storageRoot, `user_${id}`);
const userStorageDir = path.join(storageRoot, `user_${userId}`);
if (fs.existsSync(userStorageDir)) {
try {
@@ -4642,7 +5044,7 @@ app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req,
// 3. 删除用户的所有分享记录
try {
const userShares = ShareDB.getUserShares(id);
const userShares = ShareDB.getUserShares(userId);
deletionLog.deletedShares = userShares.length;
userShares.forEach(share => {
@@ -4660,7 +5062,7 @@ app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req,
}
// 4. 删除用户记录
UserDB.delete(id);
UserDB.delete(userId);
// 构建响应消息
let message = `用户 ${user.username} 已删除`;
@@ -4672,6 +5074,16 @@ app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req,
message += `,已删除 ${deletionLog.deletedShares} 条分享`;
}
// 记录管理员删除用户操作日志
logUser(req, 'delete_user', `管理员删除用户: ${user.username}`, {
targetUserId: userId,
targetUsername: user.username,
targetEmail: user.email,
deletedShares: deletionLog.deletedShares,
deletedFiles: deletionLog.deletedFiles.length,
warnings: deletionLog.warnings
});
res.json({
success: true,
message,
@@ -4731,15 +5143,24 @@ app.post('/api/admin/users/:id/storage-permission',
const { id } = req.params;
const { storage_permission, local_storage_quota } = req.body;
// 参数验证:验证 ID 格式
const userId = parseInt(id, 10);
if (isNaN(userId) || userId <= 0) {
return res.status(400).json({
success: false,
message: '无效的用户ID'
});
}
const updates = { storage_permission };
// 如果提供了配额,更新配额(单位:字节)
if (local_storage_quota !== undefined) {
updates.local_storage_quota = parseInt(local_storage_quota);
updates.local_storage_quota = parseInt(local_storage_quota, 10);
}
// 根据权限设置自动调整存储类型
const user = UserDB.findById(id);
const user = UserDB.findById(userId);
if (!user) {
return res.status(404).json({
success: false,
@@ -4757,7 +5178,7 @@ app.post('/api/admin/users/:id/storage-permission',
}
// user_choice 不自动切换,保持用户当前选择
UserDB.update(id, updates);
UserDB.update(userId, updates);
res.json({
success: true,
@@ -4781,8 +5202,17 @@ app.get('/api/admin/users/:id/files', authMiddleware, adminMiddleware, async (re
const dirPath = req.query.path || '/';
let ossClient;
// 参数验证:验证 ID 格式
const userId = parseInt(id, 10);
if (isNaN(userId) || userId <= 0) {
return res.status(400).json({
success: false,
message: '无效的用户ID'
});
}
try {
const user = UserDB.findById(id);
const user = UserDB.findById(userId);
if (!user) {
return res.status(404).json({
@@ -4798,7 +5228,7 @@ app.get('/api/admin/users/:id/files', authMiddleware, adminMiddleware, async (re
});
}
const { OssStorageClient } = require('./storage');
// OssStorageClient 已在文件顶部导入
ossClient = new OssStorageClient(user);
await ossClient.connect();
const list = await ossClient.list(dirPath);
@@ -4856,8 +5286,17 @@ app.get('/api/admin/shares', authMiddleware, adminMiddleware, (req, res) => {
// 删除分享(管理员)
app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, (req, res) => {
try {
// 参数验证:验证 ID 格式
const shareId = parseInt(req.params.id, 10);
if (isNaN(shareId) || shareId <= 0) {
return res.status(400).json({
success: false,
message: '无效的分享ID'
});
}
// 先获取分享信息以获得share_code
const share = ShareDB.findById(req.params.id);
const share = ShareDB.findById(shareId);
if (share) {
// 删除缓存
@@ -4867,7 +5306,13 @@ app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, (req, res)
}
// 删除数据库记录
ShareDB.delete(req.params.id);
ShareDB.delete(shareId);
// 记录管理员操作日志
logShare(req, 'admin_delete_share',
`管理员删除分享: ${share.share_code}`,
{ shareId, shareCode: share.share_code, sharePath: share.share_path, ownerId: share.user_id }
);
res.json({
success: true,