Files
smartmta/dist_ts/mail/delivery/smtpserver/security-handler.js
2026-02-10 15:54:09 +00:00

242 lines
18 KiB
JavaScript

/**
* SMTP Security Handler
* Responsible for security aspects including IP reputation checking,
* email validation, and authentication
*/
import * as plugins from '../../../plugins.js';
import { SmtpLogger } from './utils/logging.js';
import { SecurityEventType, SecurityLogLevel } from './constants.js';
import { isValidEmail } from './utils/validation.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
import { IPReputationChecker } from '../../../security/classes.ipreputationchecker.js';
/**
* Handles security aspects for SMTP server
*/
export class SecurityHandler {
/**
* Reference to the SMTP server instance
*/
smtpServer;
/**
* IP reputation checker service
*/
ipReputationService;
/**
* Simple in-memory IP denylist
*/
ipDenylist = [];
/**
* Cleanup interval timer
*/
cleanupInterval = null;
/**
* Creates a new security handler
* @param smtpServer - SMTP server instance
*/
constructor(smtpServer) {
this.smtpServer = smtpServer;
// Initialize IP reputation checker
this.ipReputationService = new IPReputationChecker();
// Clean expired denylist entries periodically
this.cleanupInterval = setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute
}
/**
* Check IP reputation for a connection
* @param socket - Client socket
* @returns Promise that resolves to true if IP is allowed, false if blocked
*/
async checkIpReputation(socket) {
const socketDetails = getSocketDetails(socket);
const ip = socketDetails.remoteAddress;
// Check local denylist first
if (this.isIpDenylisted(ip)) {
// Log the blocked connection
this.logSecurityEvent(SecurityEventType.IP_REPUTATION, SecurityLogLevel.WARN, `Connection blocked from denylisted IP: ${ip}`, { reason: this.getDenylistReason(ip) });
return false;
}
// Check with IP reputation service
if (!this.ipReputationService) {
return true;
}
try {
// Check with IP reputation service
const reputationResult = await this.ipReputationService.checkReputation(ip);
// Block if score is below HIGH_RISK threshold (20) or if it's spam/proxy/tor/vpn
const isBlocked = reputationResult.score < 20 ||
reputationResult.isSpam ||
reputationResult.isTor ||
reputationResult.isProxy;
if (isBlocked) {
// Add to local denylist temporarily
const reason = reputationResult.isSpam ? 'spam' :
reputationResult.isTor ? 'tor' :
reputationResult.isProxy ? 'proxy' :
`low reputation score: ${reputationResult.score}`;
this.addToDenylist(ip, reason, 3600000); // 1 hour
// Log the blocked connection
this.logSecurityEvent(SecurityEventType.IP_REPUTATION, SecurityLogLevel.WARN, `Connection blocked by reputation service: ${ip}`, {
reason,
score: reputationResult.score,
isSpam: reputationResult.isSpam,
isTor: reputationResult.isTor,
isProxy: reputationResult.isProxy,
isVPN: reputationResult.isVPN
});
return false;
}
// Log the allowed connection
this.logSecurityEvent(SecurityEventType.IP_REPUTATION, SecurityLogLevel.INFO, `IP reputation check passed: ${ip}`, {
score: reputationResult.score,
country: reputationResult.country,
org: reputationResult.org
});
return true;
}
catch (error) {
// Log the error
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
ip,
error: error instanceof Error ? error : new Error(String(error))
});
// Allow the connection on error (fail open)
return true;
}
}
/**
* Validate an email address
* @param email - Email address to validate
* @returns Whether the email address is valid
*/
isValidEmail(email) {
return isValidEmail(email);
}
/**
* Validate authentication credentials
* @param auth - Authentication credentials
* @returns Promise that resolves to true if authenticated
*/
async authenticate(auth) {
const { username, password } = auth;
// Get auth options from server
const options = this.smtpServer.getOptions();
const authOptions = options.auth;
// Check if authentication is enabled
if (!authOptions) {
this.logSecurityEvent(SecurityEventType.AUTHENTICATION, SecurityLogLevel.WARN, 'Authentication attempt when auth is disabled', { username });
return false;
}
// Note: Method validation and TLS requirement checks would need to be done
// at the caller level since the interface doesn't include session/method info
try {
let authenticated = false;
// Use custom validation function if provided
if (authOptions.validateUser) {
authenticated = await authOptions.validateUser(username, password);
}
else {
// Default behavior - no authentication
authenticated = false;
}
// Log the authentication result
this.logSecurityEvent(SecurityEventType.AUTHENTICATION, authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN, authenticated ? 'Authentication successful' : 'Authentication failed', { username });
return authenticated;
}
catch (error) {
// Log authentication error
this.logSecurityEvent(SecurityEventType.AUTHENTICATION, SecurityLogLevel.ERROR, `Authentication error: ${error instanceof Error ? error.message : String(error)}`, { username, error: error instanceof Error ? error.message : String(error) });
return false;
}
}
/**
* Log a security event
* @param event - Event type
* @param level - Log level
* @param details - Event details
*/
logSecurityEvent(event, level, message, details) {
SmtpLogger.logSecurityEvent(level, event, message, details, details.ip, details.domain, details.success);
}
/**
* Add an IP to the denylist
* @param ip - IP address
* @param reason - Reason for denylisting
* @param duration - Duration in milliseconds (optional, indefinite if not specified)
*/
addToDenylist(ip, reason, duration) {
// Remove existing entry if present
this.ipDenylist = this.ipDenylist.filter(entry => entry.ip !== ip);
// Create new entry
const entry = {
ip,
reason,
expiresAt: duration ? Date.now() + duration : undefined
};
// Add to denylist
this.ipDenylist.push(entry);
// Log the action
this.logSecurityEvent(SecurityEventType.ACCESS_CONTROL, SecurityLogLevel.INFO, `Added IP to denylist: ${ip}`, {
ip,
reason,
duration: duration ? `${duration / 1000} seconds` : 'indefinite'
});
}
/**
* Check if an IP is denylisted
* @param ip - IP address
* @returns Whether the IP is denylisted
*/
isIpDenylisted(ip) {
const entry = this.ipDenylist.find(e => e.ip === ip);
if (!entry) {
return false;
}
// Check if entry has expired
if (entry.expiresAt && entry.expiresAt < Date.now()) {
// Remove expired entry
this.ipDenylist = this.ipDenylist.filter(e => e !== entry);
return false;
}
return true;
}
/**
* Get the reason an IP was denylisted
* @param ip - IP address
* @returns Reason for denylisting or undefined if not denylisted
*/
getDenylistReason(ip) {
const entry = this.ipDenylist.find(e => e.ip === ip);
return entry?.reason;
}
/**
* Clean expired denylist entries
*/
cleanExpiredDenylistEntries() {
const now = Date.now();
const initialCount = this.ipDenylist.length;
this.ipDenylist = this.ipDenylist.filter(entry => {
return !entry.expiresAt || entry.expiresAt > now;
});
const removedCount = initialCount - this.ipDenylist.length;
if (removedCount > 0) {
this.logSecurityEvent(SecurityEventType.ACCESS_CONTROL, SecurityLogLevel.INFO, `Cleaned up ${removedCount} expired denylist entries`, { remainingCount: this.ipDenylist.length });
}
}
/**
* Clean up resources
*/
destroy() {
// Clear the cleanup interval
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
// Clear the denylist
this.ipDenylist = [];
// Clean up IP reputation service if it has a destroy method
if (this.ipReputationService && typeof this.ipReputationService.destroy === 'function') {
this.ipReputationService.destroy();
}
SmtpLogger.debug('SecurityHandler destroyed');
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VjdXJpdHktaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9zZWN1cml0eS1oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7O0dBSUc7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBRy9DLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNyRSxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDckQsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGFBQWEsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3JFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGtEQUFrRCxDQUFDO0FBV3ZGOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGVBQWU7SUFDMUI7O09BRUc7SUFDSyxVQUFVLENBQWM7SUFFaEM7O09BRUc7SUFDSyxtQkFBbUIsQ0FBc0I7SUFFakQ7O09BRUc7SUFDSyxVQUFVLEdBQXVCLEVBQUUsQ0FBQztJQUU1Qzs7T0FFRztJQUNLLGVBQWUsR0FBMEIsSUFBSSxDQUFDO0lBRXREOzs7T0FHRztJQUNILFlBQVksVUFBdUI7UUFDakMsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUM7UUFFN0IsbUNBQW1DO1FBQ25DLElBQUksQ0FBQyxtQkFBbUIsR0FBRyxJQUFJLG1CQUFtQixFQUFFLENBQUM7UUFFckQsOENBQThDO1FBQzlDLElBQUksQ0FBQyxlQUFlLEdBQUcsV0FBVyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQywyQkFBMkIsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUMsZUFBZTtJQUN0RyxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLEtBQUssQ0FBQyxpQkFB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