218 lines
7.4 KiB
TypeScript
218 lines
7.4 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
import { connectToSmtp, waitForGreeting, sendSmtpCommand, closeSmtpConnection } from '../../helpers/utils.js';
|
|
|
|
let testServer: ITestServer;
|
|
|
|
tap.test('setup - start SMTP server with authentication', async () => {
|
|
testServer = await startTestServer({
|
|
port: 2530,
|
|
hostname: 'localhost',
|
|
authRequired: true
|
|
});
|
|
expect(testServer).toBeInstanceOf(Object);
|
|
});
|
|
|
|
tap.test('SEC-01: Authentication - server advertises AUTH capability', async () => {
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
|
|
// Send EHLO to get capabilities
|
|
const ehloResponse = await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
|
|
// Parse capabilities
|
|
const lines = ehloResponse.split('\r\n').filter(line => line.length > 0);
|
|
const capabilities = lines.map(line => line.substring(4).trim());
|
|
|
|
// Check for AUTH capability
|
|
const authCapability = capabilities.find(cap => cap.startsWith('AUTH'));
|
|
expect(authCapability).toBeDefined();
|
|
|
|
// Extract supported mechanisms
|
|
const supportedMechanisms = authCapability?.substring(5).split(' ') || [];
|
|
console.log('📋 Supported AUTH mechanisms:', supportedMechanisms);
|
|
|
|
// Common mechanisms should be supported
|
|
expect(supportedMechanisms).toContain('PLAIN');
|
|
expect(supportedMechanisms).toContain('LOGIN');
|
|
|
|
console.log('✅ AUTH capability test passed');
|
|
|
|
} finally {
|
|
await closeSmtpConnection(socket);
|
|
}
|
|
});
|
|
|
|
tap.test('SEC-01: AUTH PLAIN mechanism - correct credentials', async () => {
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
|
|
// Create AUTH PLAIN credentials
|
|
// Format: base64(NULL + username + NULL + password)
|
|
const username = 'testuser';
|
|
const password = 'testpass';
|
|
const authString = Buffer.from(`\0${username}\0${password}`).toString('base64');
|
|
|
|
// Send AUTH PLAIN command
|
|
try {
|
|
const authResponse = await sendSmtpCommand(socket, `AUTH PLAIN ${authString}`);
|
|
// Server might accept (235) or reject (535) based on configuration
|
|
expect(authResponse).toMatch(/^(235|535)/);
|
|
|
|
if (authResponse.startsWith('235')) {
|
|
console.log('✅ AUTH PLAIN accepted (test mode)');
|
|
} else {
|
|
console.log('✅ AUTH PLAIN properly rejected (production mode)');
|
|
}
|
|
} catch (error) {
|
|
// Auth failure is expected in test environment
|
|
console.log('✅ AUTH PLAIN handled:', error.message);
|
|
}
|
|
|
|
} finally {
|
|
await closeSmtpConnection(socket);
|
|
}
|
|
});
|
|
|
|
tap.test('SEC-01: AUTH LOGIN mechanism - interactive authentication', async () => {
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
|
|
// Start AUTH LOGIN
|
|
try {
|
|
const authStartResponse = await sendSmtpCommand(socket, 'AUTH LOGIN', '334');
|
|
expect(authStartResponse).toInclude('334');
|
|
|
|
// Server should prompt for username (base64 "Username:")
|
|
const usernamePrompt = Buffer.from(
|
|
authStartResponse.substring(4).trim(),
|
|
'base64'
|
|
).toString();
|
|
console.log('Server prompt:', usernamePrompt);
|
|
|
|
// Send username
|
|
const username = Buffer.from('testuser').toString('base64');
|
|
const passwordPromptResponse = await sendSmtpCommand(socket, username, '334');
|
|
|
|
// Send password
|
|
const password = Buffer.from('testpass').toString('base64');
|
|
const authResult = await sendSmtpCommand(socket, password);
|
|
|
|
// Check result (235 = success, 535 = failure)
|
|
expect(authResult).toMatch(/^(235|535)/);
|
|
|
|
} catch (error) {
|
|
// Auth failure is expected in test environment
|
|
console.log('✅ AUTH LOGIN handled:', error.message);
|
|
}
|
|
|
|
} finally {
|
|
await closeSmtpConnection(socket);
|
|
}
|
|
});
|
|
|
|
tap.test('SEC-01: Authentication required - reject commands without auth', async () => {
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
|
|
// Try to send email without authentication
|
|
try {
|
|
const mailResponse = await sendSmtpCommand(socket, 'MAIL FROM:<test@example.com>');
|
|
|
|
// Server should reject with 530 (authentication required) or similar
|
|
if (mailResponse.startsWith('530') || mailResponse.startsWith('503')) {
|
|
console.log('✅ Server properly requires authentication');
|
|
} else if (mailResponse.startsWith('250')) {
|
|
console.log('⚠️ Server accepted mail without auth (test mode)');
|
|
}
|
|
|
|
} catch (error) {
|
|
// Command rejection is expected
|
|
console.log('✅ Server rejected unauthenticated command:', error.message);
|
|
}
|
|
|
|
} finally {
|
|
await closeSmtpConnection(socket);
|
|
}
|
|
});
|
|
|
|
tap.test('SEC-01: Invalid authentication attempts - rate limiting', async () => {
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
|
|
// Try multiple failed authentication attempts
|
|
const maxAttempts = 5;
|
|
let failedAttempts = 0;
|
|
let requiresTLS = false;
|
|
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
try {
|
|
// Send invalid credentials
|
|
const invalidAuth = Buffer.from('\0invalid\0wrong').toString('base64');
|
|
const response = await sendSmtpCommand(socket, `AUTH PLAIN ${invalidAuth}`);
|
|
|
|
// Check if authentication failed
|
|
if (response.startsWith('535')) {
|
|
failedAttempts++;
|
|
console.log(`Failed attempt ${i + 1}: ${response.trim()}`);
|
|
|
|
// Check if server requires TLS (common security practice)
|
|
if (response.includes('TLS')) {
|
|
requiresTLS = true;
|
|
console.log('✅ Server enforces TLS requirement for authentication');
|
|
break;
|
|
}
|
|
} else if (response.startsWith('503')) {
|
|
// Too many failed attempts
|
|
failedAttempts++;
|
|
console.log('✅ Server enforces auth attempt limits');
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
// Handle connection errors
|
|
failedAttempts++;
|
|
console.log(`Failed attempt ${i + 1}: ${error.message}`);
|
|
|
|
// Check if server closed connection or rate limited
|
|
if (error.message.includes('closed') || error.message.includes('timeout')) {
|
|
console.log('✅ Server enforces auth attempt limits by closing connection');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Either TLS is required or we had failed attempts
|
|
expect(failedAttempts).toBeGreaterThan(0);
|
|
if (requiresTLS) {
|
|
console.log('✅ Authentication properly protected by TLS requirement');
|
|
} else {
|
|
console.log(`✅ Handled ${failedAttempts} failed auth attempts`);
|
|
}
|
|
|
|
} finally {
|
|
if (!socket.destroyed) {
|
|
socket.destroy();
|
|
}
|
|
}
|
|
});
|
|
|
|
tap.test('cleanup - stop SMTP server', async () => {
|
|
await stopTestServer(testServer);
|
|
console.log('✅ Test server stopped');
|
|
});
|
|
|
|
export default tap.start(); |