fix: 修复分享过期时间和防爆破保护覆盖不全的安全问题

问题1 - 分享过期时间未强制校验:
- 在ShareDB.findByCode()中添加过期时间检查
- SQL条件: AND (s.expires_at IS NULL OR s.expires_at > datetime('now'))
- 现在过期的分享链接将返回404,无法访问

问题2 - 分享密码防爆破保护覆盖不全:
- 给/api/share/:code/list添加shareRateLimitMiddleware
- 给/api/share/:code/download-file添加shareRateLimitMiddleware
- 在两个接口的密码验证失败时调用recordFailure
- 在两个接口的密码验证成功时调用recordSuccess
- 防止攻击者绕过/verify接口直接暴力破解

影响:
- 分享过期后将无法访问(安全性提升)
- 所有分享密码验证接口都受到限流保护(10次/10分钟)
- 修复了可绕过防爆破保护的安全漏洞
This commit is contained in:
2025-11-14 00:15:15 +08:00
parent 53be6dc145
commit 4879d4891f
2 changed files with 21 additions and 2 deletions

View File

@@ -337,6 +337,7 @@ const ShareDB = {
FROM shares s FROM shares s
JOIN users u ON s.user_id = u.id JOIN users u ON s.user_id = u.id
WHERE s.share_code = ? WHERE s.share_code = ?
AND (s.expires_at IS NULL OR s.expires_at > datetime('now'))
`).get(shareCode); `).get(shareCode);
}, },

View File

@@ -1735,7 +1735,7 @@ app.post('/api/share/:code/verify', shareRateLimitMiddleware, async (req, res) =
}); });
// 获取分享的文件列表支持本地存储和SFTP // 获取分享的文件列表支持本地存储和SFTP
app.post('/api/share/:code/list', async (req, res) => { app.post('/api/share/:code/list', shareRateLimitMiddleware, async (req, res) => {
const { code } = req.params; const { code } = req.params;
const { password, path: subPath } = req.body; const { password, path: subPath } = req.body;
@@ -1753,12 +1753,21 @@ app.post('/api/share/:code/list', async (req, res) => {
// 验证密码 // 验证密码
if (share.share_password && !ShareDB.verifyPassword(password, share.share_password)) { if (share.share_password && !ShareDB.verifyPassword(password, share.share_password)) {
// 记录密码错误
if (req.shareRateLimitKey) {
shareLimiter.recordFailure(req.shareRateLimitKey);
}
return res.status(401).json({ return res.status(401).json({
success: false, success: false,
message: '密码错误' message: '密码错误'
}); });
} }
// 清除失败记录(密码验证成功或无密码)
if (req.shareRateLimitKey && share.share_password) {
shareLimiter.recordSuccess(req.shareRateLimitKey);
}
// 获取分享者的用户信息 // 获取分享者的用户信息
const shareOwner = UserDB.findById(share.user_id); const shareOwner = UserDB.findById(share.user_id);
if (!shareOwner) { if (!shareOwner) {
@@ -1912,7 +1921,7 @@ app.post('/api/share/:code/download', (req, res) => {
}); });
// 分享文件下载支持本地存储和SFTP公开API需要分享码和密码验证 // 分享文件下载支持本地存储和SFTP公开API需要分享码和密码验证
app.get('/api/share/:code/download-file', async (req, res) => { app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req, res) => {
const { code } = req.params; const { code } = req.params;
const { path: filePath, password } = req.query; const { path: filePath, password } = req.query;
let storage; let storage;
@@ -1936,11 +1945,20 @@ app.get('/api/share/:code/download-file', async (req, res) => {
// 验证密码(如果需要) // 验证密码(如果需要)
if (share.share_password) { if (share.share_password) {
// 记录密码错误
if (req.shareRateLimitKey) {
shareLimiter.recordFailure(req.shareRateLimitKey);
}
if (!password || !ShareDB.verifyPassword(password, share.share_password)) { if (!password || !ShareDB.verifyPassword(password, share.share_password)) {
return res.status(401).json({ return res.status(401).json({
success: false, success: false,
message: '密码错误或未提供密码' message: '密码错误或未提供密码'
}); });
// 清除失败记录(密码验证成功)
if (req.shareRateLimitKey && share.share_password) {
shareLimiter.recordSuccess(req.shareRateLimitKey);
}
} }
} }