dcrouter/test/suite/smtpclient_security/test.csec-09.relay-restrictions.ts

627 lines
22 KiB
TypeScript
Raw Normal View History

2025-05-24 17:00:59 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from './plugins.js';
import { createTestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
tap.test('CSEC-09: should handle relay restrictions correctly', async (tools) => {
const testId = 'CSEC-09-relay-restrictions';
console.log(`\n${testId}: Testing relay restriction handling...`);
let scenarioCount = 0;
// Scenario 1: Open relay prevention
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing open relay prevention`);
const allowedDomains = ['example.com', 'trusted.com'];
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 relay.example.com ESMTP\r\n');
let authenticated = false;
let fromAddress = '';
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-relay.example.com\r\n');
socket.write('250-AUTH PLAIN LOGIN\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('AUTH')) {
authenticated = true;
console.log(' [Server] User authenticated');
socket.write('235 2.7.0 Authentication successful\r\n');
} else if (command.startsWith('MAIL FROM:')) {
fromAddress = command.match(/<(.+)>/)?.[1] || '';
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
const toAddress = command.match(/<(.+)>/)?.[1] || '';
const toDomain = toAddress.split('@')[1];
const fromDomain = fromAddress.split('@')[1];
console.log(` [Server] Relay check: from=${fromDomain}, to=${toDomain}, auth=${authenticated}`);
// Check relay permissions
if (authenticated) {
// Authenticated users can relay
socket.write('250 OK\r\n');
} else if (allowedDomains.includes(toDomain)) {
// Accept mail for local domains
socket.write('250 OK\r\n');
} else if (allowedDomains.includes(fromDomain)) {
// Accept mail from local domains (outbound)
socket.write('250 OK\r\n');
} else {
// Reject relay attempt
console.log(' [Server] Rejecting relay attempt');
socket.write('554 5.7.1 Relay access denied\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Test 1: Unauthenticated relay attempt (should fail)
const unauthClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const relayEmail = new plugins.smartmail.Email({
from: 'external@untrusted.com',
to: ['recipient@another-external.com'],
subject: 'Relay test',
text: 'Testing open relay prevention'
});
try {
await unauthClient.sendMail(relayEmail);
console.log(' Unexpected: Relay was allowed');
} catch (error) {
console.log(' Expected: Relay denied for unauthenticated user');
expect(error.message).toContain('Relay access denied');
}
// Test 2: Local delivery (should succeed)
const localEmail = new plugins.smartmail.Email({
from: 'external@untrusted.com',
to: ['recipient@example.com'], // Local domain
subject: 'Local delivery test',
text: 'Testing local delivery'
});
const localResult = await unauthClient.sendMail(localEmail);
console.log(' Local delivery allowed');
expect(localResult).toBeDefined();
expect(localResult.messageId).toBeDefined();
// Test 3: Authenticated relay (should succeed)
const authClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
auth: {
user: 'testuser',
pass: 'testpass'
}
});
const authRelayResult = await authClient.sendMail(relayEmail);
console.log(' Authenticated relay allowed');
expect(authRelayResult).toBeDefined();
expect(authRelayResult.messageId).toBeDefined();
await testServer.server.close();
})();
// Scenario 2: IP-based relay restrictions
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing IP-based relay restrictions`);
const trustedIPs = ['127.0.0.1', '::1', '10.0.0.0/8', '192.168.0.0/16'];
const testServer = await createTestServer({
onConnection: async (socket) => {
const clientIP = socket.remoteAddress || '';
console.log(` [Server] Client connected from ${clientIP}`);
socket.write('220 ip-relay.example.com ESMTP\r\n');
const isTrustedIP = (ip: string): boolean => {
// Simple check for demo (in production, use proper IP range checking)
return trustedIPs.some(trusted =>
ip === trusted ||
ip.includes('127.0.0.1') ||
ip.includes('::1')
);
};
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-ip-relay.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
const toAddress = command.match(/<(.+)>/)?.[1] || '';
const isLocalDomain = toAddress.includes('@example.com');
if (isTrustedIP(clientIP)) {
console.log(' [Server] Trusted IP - allowing relay');
socket.write('250 OK\r\n');
} else if (isLocalDomain) {
console.log(' [Server] Local delivery - allowing');
socket.write('250 OK\r\n');
} else {
console.log(' [Server] Untrusted IP - denying relay');
socket.write('554 5.7.1 Relay access denied for IP\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test from localhost (trusted)
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@external.com'],
subject: 'IP-based relay test',
text: 'Testing IP-based relay restrictions'
});
const result = await smtpClient.sendMail(email);
console.log(' Relay allowed from trusted IP (localhost)');
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
})();
// Scenario 3: Sender domain restrictions
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing sender domain restrictions`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 sender-restrict.example.com ESMTP\r\n');
let authenticated = false;
let authUser = '';
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-sender-restrict.example.com\r\n');
socket.write('250-AUTH PLAIN\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('AUTH PLAIN')) {
const credentials = command.substring(11);
if (credentials) {
const decoded = Buffer.from(credentials, 'base64').toString();
authUser = decoded.split('\0')[1] || '';
authenticated = true;
console.log(` [Server] User authenticated: ${authUser}`);
socket.write('235 2.7.0 Authentication successful\r\n');
} else {
socket.write('334\r\n');
}
} else if (!command.startsWith('AUTH') && authenticated === false && command.length > 20) {
// PLAIN auth credentials
const decoded = Buffer.from(command, 'base64').toString();
authUser = decoded.split('\0')[1] || '';
authenticated = true;
console.log(` [Server] User authenticated: ${authUser}`);
socket.write('235 2.7.0 Authentication successful\r\n');
} else if (command.startsWith('MAIL FROM:')) {
const fromAddress = command.match(/<(.+)>/)?.[1] || '';
const fromDomain = fromAddress.split('@')[1];
if (!authenticated) {
// Unauthenticated users can only send from specific domains
if (fromDomain === 'example.com' || fromDomain === 'trusted.com') {
socket.write('250 OK\r\n');
} else {
console.log(` [Server] Rejecting sender domain: ${fromDomain}`);
socket.write('553 5.7.1 Sender domain not allowed\r\n');
}
} else {
// Authenticated users must use their own domain
const expectedDomain = authUser.split('@')[1];
if (fromDomain === expectedDomain || fromDomain === 'example.com') {
socket.write('250 OK\r\n');
} else {
console.log(` [Server] Auth user ${authUser} cannot send from ${fromDomain}`);
socket.write('553 5.7.1 Authenticated sender mismatch\r\n');
}
}
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Test 1: Unauthorized sender domain
const unauthClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const unauthorizedEmail = new plugins.smartmail.Email({
from: 'sender@untrusted.com',
to: ['recipient@example.com'],
subject: 'Unauthorized sender test',
text: 'Testing sender domain restrictions'
});
try {
await unauthClient.sendMail(unauthorizedEmail);
console.log(' Unexpected: Unauthorized sender accepted');
} catch (error) {
console.log(' Expected: Unauthorized sender domain rejected');
expect(error.message).toContain('Sender domain not allowed');
}
// Test 2: Authorized sender domain
const authorizedEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@external.com'],
subject: 'Authorized sender test',
text: 'Testing authorized sender domain'
});
const result = await unauthClient.sendMail(authorizedEmail);
console.log(' Authorized sender domain accepted');
expect(result).toBeDefined();
// Test 3: Authenticated sender mismatch
const authClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
auth: {
user: 'user@example.com',
pass: 'testpass'
}
});
const mismatchEmail = new plugins.smartmail.Email({
from: 'someone@otherdomain.com',
to: ['recipient@example.com'],
subject: 'Sender mismatch test',
text: 'Testing authenticated sender mismatch'
});
try {
await authClient.sendMail(mismatchEmail);
console.log(' Unexpected: Sender mismatch accepted');
} catch (error) {
console.log(' Expected: Authenticated sender mismatch rejected');
expect(error.message).toContain('Authenticated sender mismatch');
}
await testServer.server.close();
})();
// Scenario 4: Recipient count limits
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing recipient count limits`);
const maxRecipientsUnauthenticated = 5;
const maxRecipientsAuthenticated = 100;
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 recipient-limit.example.com ESMTP\r\n');
let authenticated = false;
let recipientCount = 0;
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-recipient-limit.example.com\r\n');
socket.write('250-AUTH PLAIN\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('AUTH')) {
authenticated = true;
socket.write('235 2.7.0 Authentication successful\r\n');
} else if (command.startsWith('MAIL FROM:')) {
recipientCount = 0; // Reset for new message
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
recipientCount++;
const limit = authenticated ? maxRecipientsAuthenticated : maxRecipientsUnauthenticated;
console.log(` [Server] Recipient ${recipientCount}/${limit} (auth: ${authenticated})`);
if (recipientCount > limit) {
socket.write('452 4.5.3 Too many recipients\r\n');
} else {
socket.write('250 OK\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Test unauthenticated recipient limit
const unauthClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const manyRecipients = Array(10).fill(null).map((_, i) => `recipient${i + 1}@example.com`);
const bulkEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: manyRecipients,
subject: 'Recipient limit test',
text: 'Testing recipient count limits'
});
try {
const result = await unauthClient.sendMail(bulkEmail);
console.log(` Sent to ${result.accepted?.length || 0} recipients (unauthenticated)`);
// Some recipients should be rejected
expect(result.rejected?.length).toBeGreaterThan(0);
} catch (error) {
console.log(' Some recipients rejected due to limit');
}
// Test authenticated higher limit
const authClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
auth: {
user: 'testuser',
pass: 'testpass'
}
});
const authResult = await authClient.sendMail(bulkEmail);
console.log(` Authenticated user sent to ${authResult.accepted?.length || 0} recipients`);
expect(authResult.accepted?.length).toBe(manyRecipients.length);
await testServer.server.close();
})();
// Scenario 5: Rate-based relay restrictions
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing rate-based relay restrictions`);
const messageRates = new Map<string, { count: number; resetTime: number }>();
const rateLimit = 3; // 3 messages per minute
const testServer = await createTestServer({
onConnection: async (socket) => {
const clientIP = socket.remoteAddress || 'unknown';
console.log(` [Server] Client connected from ${clientIP}`);
socket.write('220 rate-limit.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-rate-limit.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
const now = Date.now();
const clientRate = messageRates.get(clientIP) || { count: 0, resetTime: now + 60000 };
if (now > clientRate.resetTime) {
// Reset rate limit
clientRate.count = 0;
clientRate.resetTime = now + 60000;
}
clientRate.count++;
messageRates.set(clientIP, clientRate);
console.log(` [Server] Message ${clientRate.count}/${rateLimit} from ${clientIP}`);
if (clientRate.count > rateLimit) {
socket.write('421 4.7.0 Rate limit exceeded, try again later\r\n');
socket.end();
} else {
socket.write('250 OK\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Send multiple messages to test rate limiting
const results: boolean[] = [];
for (let i = 0; i < 5; i++) {
try {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Rate test ${i + 1}`,
text: `Testing rate limits - message ${i + 1}`
});
const result = await client.sendMail(email);
console.log(` Message ${i + 1}: Sent successfully`);
results.push(true);
} catch (error) {
console.log(` Message ${i + 1}: Rate limited`);
results.push(false);
}
}
// First 3 should succeed, rest should fail
const successCount = results.filter(r => r).length;
console.log(` Sent ${successCount}/${results.length} messages before rate limit`);
expect(successCount).toBe(rateLimit);
await testServer.server.close();
})();
// Scenario 6: SPF-based relay restrictions
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing SPF-based relay restrictions`);
const testServer = await createTestServer({
onConnection: async (socket) => {
const clientIP = socket.remoteAddress || '';
console.log(` [Server] Client connected from ${clientIP}`);
socket.write('220 spf-relay.example.com ESMTP\r\n');
const checkSPF = (domain: string, ip: string): string => {
// Simplified SPF check for demo
console.log(` [Server] Checking SPF for ${domain} from ${ip}`);
// In production, would do actual DNS lookups
if (domain === 'example.com' && (ip.includes('127.0.0.1') || ip.includes('::1'))) {
return 'pass';
} else if (domain === 'spf-fail.com') {
return 'fail';
} else {
return 'none';
}
};
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-spf-relay.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
const fromAddress = command.match(/<(.+)>/)?.[1] || '';
const domain = fromAddress.split('@')[1];
const spfResult = checkSPF(domain, clientIP);
console.log(` [Server] SPF result: ${spfResult}`);
if (spfResult === 'fail') {
socket.write('550 5.7.1 SPF check failed\r\n');
} else {
socket.write('250 OK SPF=' + spfResult + '\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test 1: SPF pass
const spfPassEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'SPF pass test',
text: 'Testing SPF-based relay - should pass'
});
const passResult = await smtpClient.sendMail(spfPassEmail);
console.log(' SPF check passed');
expect(passResult).toBeDefined();
expect(passResult.response).toContain('SPF=pass');
// Test 2: SPF fail
const spfFailEmail = new plugins.smartmail.Email({
from: 'sender@spf-fail.com',
to: ['recipient@example.com'],
subject: 'SPF fail test',
text: 'Testing SPF-based relay - should fail'
});
try {
await smtpClient.sendMail(spfFailEmail);
console.log(' Unexpected: SPF fail was accepted');
} catch (error) {
console.log(' Expected: SPF check failed');
expect(error.message).toContain('SPF check failed');
}
await testServer.server.close();
})();
console.log(`\n${testId}: All ${scenarioCount} relay restriction scenarios tested ✓`);
});