import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import * as net from 'net'; import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../../helpers/server.loader.js'; import type { SmtpServer } from '../../../ts/mail/delivery/smtpserver/index.js'; let testServer: SmtpServer; tap.test('setup - start test server', async () => { testServer = await startTestServer(); await plugins.smartdelay.delayFor(1000); }); tap.test('Header Injection Prevention - CRLF injection in headers', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo'; socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo' && dataBuffer.includes('250')) { step = 'mail'; socket.write('MAIL FROM:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\r\n'); dataBuffer = ''; } else if (step === 'rcpt' && dataBuffer.includes('250')) { step = 'data'; socket.write('DATA\r\n'); dataBuffer = ''; } else if (step === 'data' && dataBuffer.includes('354')) { // Attempt header injection with CRLF sequences const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: Test\r\nBcc: hidden@attacker.com`, // CRLF injection attempt `Date: ${new Date().toUTCString()}`, `Message-ID: `, `X-Custom: normal\r\nX-Injected: malicious`, // Another injection attempt '', 'This email tests header injection prevention.', '.', '' ].join('\r\n'); socket.write(email); dataBuffer = ''; } else if (dataBuffer.includes('250 ') || dataBuffer.includes('550 ')) { const accepted = dataBuffer.includes('250'); const rejected = dataBuffer.includes('550'); console.log(`Header injection attempt: ${accepted ? 'accepted' : 'rejected'}`); if (rejected) { console.log('Header injection prevention active - malicious headers detected'); } expect(accepted || rejected).toEqual(true); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('Header Injection Prevention - Command injection in MAIL FROM', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo'; socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo' && dataBuffer.includes('250')) { step = 'mail'; // Attempt command injection in MAIL FROM socket.write('MAIL FROM: SIZE=1000\r\nRCPT TO:\r\n'); dataBuffer = ''; } else if (step === 'mail') { // Server should reject or handle this properly const properResponse = dataBuffer.includes('250') || dataBuffer.includes('501') || dataBuffer.includes('500'); console.log('Command injection attempt handled'); expect(properResponse).toEqual(true); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('Header Injection Prevention - HTML/Script injection in body', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo'; socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo' && dataBuffer.includes('250')) { step = 'mail'; socket.write('MAIL FROM:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\r\n'); dataBuffer = ''; } else if (step === 'rcpt' && dataBuffer.includes('250')) { step = 'data'; socket.write('DATA\r\n'); dataBuffer = ''; } else if (step === 'data' && dataBuffer.includes('354')) { // Email with HTML/Script content const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: HTML Injection Test`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, `Content-Type: text/html`, '', '', '

Test Email

', '', '', 'Injected-Header: malicious-value', // Attempted header injection in body '', '.', '' ].join('\r\n'); socket.write(email); dataBuffer = ''; } else if (dataBuffer.includes('250 ') || dataBuffer.includes('550 ')) { const accepted = dataBuffer.includes('250'); console.log(`HTML/Script content: ${accepted ? 'accepted (may be sanitized)' : 'rejected'}`); expect(true).toEqual(true); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('Header Injection Prevention - Null byte injection', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo'; socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo' && dataBuffer.includes('250')) { step = 'mail'; // Attempt null byte injection socket.write('MAIL FROM:\r\n'); dataBuffer = ''; } else if (step === 'mail') { // Should be rejected or sanitized const handled = dataBuffer.includes('250') || dataBuffer.includes('501') || dataBuffer.includes('550'); console.log('Null byte injection attempt handled'); expect(handled).toEqual(true); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('Header Injection Prevention - Unicode and encoding attacks', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo'; socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo' && dataBuffer.includes('250')) { step = 'mail'; socket.write('MAIL FROM:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\r\n'); dataBuffer = ''; } else if (step === 'rcpt' && dataBuffer.includes('250')) { step = 'data'; socket.write('DATA\r\n'); dataBuffer = ''; } else if (step === 'data' && dataBuffer.includes('354')) { // Unicode tricks and encoding attacks const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: =?UTF-8?B?${Buffer.from('Test\r\nBcc: hidden@attacker.com').toString('base64')}?=`, // Encoded injection `Date: ${new Date().toUTCString()}`, `Message-ID: `, `X-Test: \u000D\u000AX-Injected: true`, // Unicode CRLF '', 'Testing unicode and encoding attacks.', '\x00\x0D\x0AExtra-Header: injected', // Null byte + CRLF '.', '' ].join('\r\n'); socket.write(email); dataBuffer = ''; } else if (dataBuffer.includes('250 ') || dataBuffer.includes('550 ')) { const result = dataBuffer.includes('250') ? 'accepted' : 'rejected'; console.log(`Unicode/encoding attack: ${result}`); expect(true).toEqual(true); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('cleanup - stop test server', async () => { await stopTestServer(testServer); }); tap.start();