feat(backend): 实施连接与数据层速率限制

为了增强服务的稳定性和安全性,防止滥用行为,本次提交引入了两个核心的速率限制和验证机制:

1. **连接层验证 (`onConnect`)**
   - **IP 速率限制**:在 `connectionValidator.js` 中实现。限制单个 IP 地址每分钟最多 20 次连接,超过限制将临时封禁 5 分钟,以防御暴力连接攻击。
   - **反向 DNS (PTR) 检查**:要求所有公共网络连接必须具有有效的 PTR 记录,用于区分合法邮件服务器和僵尸网络。

2. **数据层验证 (`onData`)**
   - **发件人域速率限制**:在 `rateLimiter.js` 中实现。在邮件数据传输阶段,限制单个发件人域名每分钟最多发送 10 封邮件,超过限制的域名将被临时封禁 5 分钟,以防止单一来源的邮件泛滥。

**主要变更:**
- 新增 `backend/connectionValidator.js`:处理连接时的 IP 速率限制和 PTR 验证。
- 新增 `backend/rateLimiter.js`:在 `onData` 阶段对发件人域名进行速率限制。
- 在 `backend/app.js` 的 `SMTPServer` 配置中集成了 `onConnect` 验证钩子。
- 在 `backend/saveEmail.js` 中调用速率限制器,并在超限时拒绝邮件。
- 更新日志模块 (`logger.js`, `db.js`),输出更清晰,并记录安全相关事件。
- 新增 `backend/SECURITY_POLICIES.md` 文档,详细说明所有安全策略和可配置参数。
This commit is contained in:
2025-08-01 22:59:40 +08:00
parent 3ad3849fde
commit be59fa85de
7 changed files with 338 additions and 11 deletions
+67
View File
@@ -0,0 +1,67 @@
const logger = require('./logger');
const BANNED_DOMAINS = new Set();
const DOMAIN_REQUEST_COUNTS = new Map();
const RATE_LIMIT = 10; // 每分钟10封邮件
const TIME_WINDOW = 60 * 1000; // 1分钟
const BAN_DURATION = 5 * 60 * 1000; // 5分钟
function getDomainFromEmail(sender) {
if (!sender) {
return null;
}
let emailAddress = sender;
// 检查 "Name <email@domain.com>" 格式
const match = sender.match(/<([^>]+)>/);
if (match && match[1]) {
emailAddress = match[1]; // 提取 'email@domain.com'
}
// 现在,从(可能已清理的)电子邮件地址中提取域名
if (!emailAddress.includes('@')) {
return null;
}
return emailAddress.split('@')[1];
}
function isRateLimited(sender) {
const domain = getDomainFromEmail(sender);
if (!domain) {
// 如果无法从发件人中提取域名,则不进行速率限制
return false;
}
if (BANNED_DOMAINS.has(domain)) {
logger.warn(`Domain ${domain} is currently banned, rejecting email.`, { domain, action: 'reject-banned-domain' });
return true;
}
const now = Date.now();
const requests = DOMAIN_REQUEST_COUNTS.get(domain) || [];
// 过滤掉时间窗口之外的旧请求
const recentRequests = requests.filter(timestamp => now - timestamp < TIME_WINDOW);
if (recentRequests.length >= RATE_LIMIT) {
logger.warn(`Domain ${domain} has exceeded the rate limit. Banning for ${BAN_DURATION / 1000} seconds.`, { domain, action: 'ban-domain' });
BANNED_DOMAINS.add(domain);
// 设置解封计时器
setTimeout(() => {
BANNED_DOMAINS.delete(domain);
logger.info(`Domain ${domain} has been unbanned.`, { domain, action: 'unban-domain' });
}, BAN_DURATION);
// 清空该域名的请求记录
DOMAIN_REQUEST_COUNTS.delete(domain);
return true;
}
// 记录当前请求时间
recentRequests.push(now);
DOMAIN_REQUEST_COUNTS.set(domain, recentRequests);
return false;
}
module.exports = { isRateLimited };