be59fa85de
为了增强服务的稳定性和安全性,防止滥用行为,本次提交引入了两个核心的速率限制和验证机制: 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` 文档,详细说明所有安全策略和可配置参数。
101 lines
3.6 KiB
JavaScript
101 lines
3.6 KiB
JavaScript
const dns = require('dns').promises;
|
|
const logger = require('./logger');
|
|
|
|
const IP_CONNECTION_COUNTS = new Map();
|
|
const BANNED_IPS = new Set();
|
|
|
|
const IP_RATE_LIMIT = 20; // 每个IP(服务器)每分钟最多20次连接
|
|
const IP_TIME_WINDOW = 60 * 1000; // 1分钟
|
|
const IP_BAN_DURATION = 5 * 60 * 1000; // 5分钟
|
|
|
|
/**
|
|
* 检查IP地址是否因为连接频率过高而被限制。
|
|
* @param {string} remoteAddress 客户端IP地址。
|
|
* @returns {boolean} 如果被限制则返回true,否则返回false。
|
|
*/
|
|
function isIpRateLimited(remoteAddress) {
|
|
if (BANNED_IPS.has(remoteAddress)) {
|
|
logger.warn(`Connection from banned IP ${remoteAddress} rejected.`);
|
|
return true;
|
|
}
|
|
|
|
const now = Date.now();
|
|
const requests = IP_CONNECTION_COUNTS.get(remoteAddress) || [];
|
|
const recentRequests = requests.filter(timestamp => now - timestamp < IP_TIME_WINDOW);
|
|
|
|
if (recentRequests.length >= IP_RATE_LIMIT) {
|
|
logger.warn(`IP ${remoteAddress} has exceeded the connection rate limit. Banning for ${IP_BAN_DURATION / 1000} seconds.`);
|
|
BANNED_IPS.add(remoteAddress);
|
|
setTimeout(() => {
|
|
BANNED_IPS.delete(remoteAddress);
|
|
logger.info(`IP ${remoteAddress} has been unbanned.`);
|
|
}, IP_BAN_DURATION);
|
|
IP_CONNECTION_COUNTS.delete(remoteAddress);
|
|
return true;
|
|
}
|
|
|
|
recentRequests.push(now);
|
|
IP_CONNECTION_COUNTS.set(remoteAddress, recentRequests);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 检查IP地址是否有有效的反向DNS(PTR)记录。
|
|
* 正规的邮件服务器通常都有PTR记录。
|
|
* @param {string} remoteAddress 客户端IP地址。
|
|
* @returns {Promise<boolean>} 如果验证通过则返回true,否则返回false。
|
|
*/
|
|
async function hasValidPtrRecord(remoteAddress) {
|
|
// 对于本地和私有地址,我们跳过检查,因为它们通常没有公共PTR记录
|
|
if (remoteAddress.startsWith('127.') || remoteAddress.startsWith('192.168.') || remoteAddress.startsWith('10.') || remoteAddress.startsWith('::1')) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const hostnames = await dns.reverse(remoteAddress);
|
|
if (hostnames && hostnames.length > 0) {
|
|
logger.info(`PTR record for ${remoteAddress} found: ${hostnames.join(', ')}`);
|
|
return true;
|
|
}
|
|
logger.warn(`No PTR record found for ${remoteAddress}.`);
|
|
return false;
|
|
} catch (error) {
|
|
// 'ENOTFOUND' 是最常见的错误,意味着没有找到PTR记录。
|
|
if (error.code === 'ENOTFOUND') {
|
|
logger.warn(`No PTR record found for ${remoteAddress}.`);
|
|
} else {
|
|
logger.error(`Error during PTR lookup for ${remoteAddress}:`, error);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 在连接建立时验证客户端。
|
|
* @param {object} session SMTP会话对象。
|
|
* @param {function} callback 回调函数。
|
|
*/
|
|
async function validateConnection(session, callback) {
|
|
const { remoteAddress } = session;
|
|
|
|
// 1. IP频率限制检查
|
|
if (isIpRateLimited(remoteAddress)) {
|
|
const err = new Error('Connection rejected due to high frequency. Please try again later.');
|
|
err.responseCode = 421;
|
|
return callback(err);
|
|
}
|
|
|
|
// 2. 反向DNS检查
|
|
const hasPtr = await hasValidPtrRecord(remoteAddress);
|
|
if (!hasPtr) {
|
|
const err = new Error('Connection rejected: The IP address has no PTR record.');
|
|
err.responseCode = 550; // 550表示请求的操作未执行,邮箱不可用(在这里引申为连接源不可信)
|
|
return callback(err);
|
|
}
|
|
|
|
// 所有检查通过
|
|
callback();
|
|
}
|
|
|
|
module.exports = { validateConnection };
|