import { tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { PerformanceTracker } from '../performance.tracker.js'; import * as path from 'path'; import * as os from 'os'; const performanceTracker = new PerformanceTracker('SEC-05: Path Traversal Prevention'); tap.test('SEC-05: Path Traversal Prevention - should prevent directory traversal attacks', async (t) => { const einvoice = new EInvoice(); // Test 1: Basic path traversal attempts const basicPathTraversal = await performanceTracker.measureAsync( 'basic-path-traversal-prevention', async () => { const traversalPaths = [ '../../../etc/passwd', '..\\..\\..\\windows\\system32\\config\\sam', '../../../../../../../../etc/shadow', './../.../.././../etc/hosts', '..%2F..%2F..%2Fetc%2Fpasswd', '..%252f..%252f..%252fetc%252fpasswd' ]; const results = []; for (const maliciousPath of traversalPaths) { try { // Test file read operation const canRead = await einvoice.canAccessFile(maliciousPath); // Test file write operation const canWrite = await einvoice.canWriteFile(maliciousPath); // Test path resolution const resolvedPath = await einvoice.resolvePath(maliciousPath); results.push({ path: maliciousPath, blocked: !canRead && !canWrite, resolved: resolvedPath, containsTraversal: resolvedPath?.includes('..') || false }); } catch (error) { results.push({ path: maliciousPath, blocked: true, error: error.message }); } } return results; } ); basicPathTraversal.forEach(result => { t.ok(result.blocked, `Path traversal blocked: ${result.path}`); t.notOk(result.containsTraversal, 'Resolved path does not contain traversal sequences'); }); // Test 2: Unicode and encoding bypass attempts const encodingBypass = await performanceTracker.measureAsync( 'encoding-bypass-attempts', async () => { const encodedPaths = [ '..%c0%af..%c0%afetc%c0%afpasswd', // Overlong UTF-8 '..%25c0%25af..%25c0%25afetc%25c0%25afpasswd', // Double encoding '..%c1%9c..%c1%9cetc%c1%9cpasswd', // Invalid UTF-8 '\u002e\u002e/\u002e\u002e/etc/passwd', // Unicode dots '..%u002f..%u002fetc%u002fpasswd', // IIS Unicode '..%255c..%255c..%255cwindows%255csystem32' // Double encoded backslash ]; const results = []; for (const encodedPath of encodedPaths) { try { const normalized = await einvoice.normalizePath(encodedPath); const isSafe = await einvoice.isPathSafe(normalized); results.push({ original: encodedPath, normalized, safe: isSafe, blocked: !isSafe }); } catch (error) { results.push({ original: encodedPath, blocked: true, error: error.message }); } } return results; } ); encodingBypass.forEach(result => { t.ok(result.blocked || !result.safe, `Encoded path traversal blocked: ${result.original.substring(0, 30)}...`); }); // Test 3: Null byte injection const nullByteInjection = await performanceTracker.measureAsync( 'null-byte-injection', async () => { const nullBytePaths = [ 'invoice.pdf\x00.txt', 'report.xml\x00.exe', 'document\x00../../../etc/passwd', 'file.pdf%00.jsp', 'data\u0000../../../../sensitive.dat' ]; const results = []; for (const nullPath of nullBytePaths) { try { const cleaned = await einvoice.cleanPath(nullPath); const hasNullByte = cleaned.includes('\x00') || cleaned.includes('%00'); results.push({ original: nullPath.replace(/\x00/g, '\\x00'), cleaned, nullByteRemoved: !hasNullByte, safe: !hasNullByte && !cleaned.includes('..') }); } catch (error) { results.push({ original: nullPath.replace(/\x00/g, '\\x00'), blocked: true, error: error.message }); } } return results; } ); nullByteInjection.forEach(result => { t.ok(result.nullByteRemoved || result.blocked, `Null byte injection prevented: ${result.original}`); }); // Test 4: Symbolic link attacks const symlinkAttacks = await performanceTracker.measureAsync( 'symlink-attack-prevention', async () => { const symlinkPaths = [ '/tmp/invoice_link -> /etc/passwd', 'C:\\temp\\report.lnk', './uploads/../../sensitive/data', 'invoices/current -> /home/user/.ssh/id_rsa' ]; const results = []; for (const linkPath of symlinkPaths) { try { const isSymlink = await einvoice.detectSymlink(linkPath); const followsSymlinks = await einvoice.followsSymlinks(); results.push({ path: linkPath, isSymlink, followsSymlinks, safe: !isSymlink || !followsSymlinks }); } catch (error) { results.push({ path: linkPath, safe: true, error: error.message }); } } return results; } ); symlinkAttacks.forEach(result => { t.ok(result.safe, `Symlink attack prevented: ${result.path}`); }); // Test 5: Absolute path injection const absolutePathInjection = await performanceTracker.measureAsync( 'absolute-path-injection', async () => { const absolutePaths = [ '/etc/passwd', 'C:\\Windows\\System32\\config\\SAM', '\\\\server\\share\\sensitive.dat', 'file:///etc/shadow', os.platform() === 'win32' ? 'C:\\Users\\Admin\\Documents' : '/home/user/.ssh/' ]; const results = []; for (const absPath of absolutePaths) { try { const isAllowed = await einvoice.isAbsolutePathAllowed(absPath); const normalized = await einvoice.normalizeToSafePath(absPath); results.push({ path: absPath, allowed: isAllowed, normalized, blocked: !isAllowed }); } catch (error) { results.push({ path: absPath, blocked: true, error: error.message }); } } return results; } ); absolutePathInjection.forEach(result => { t.ok(result.blocked, `Absolute path injection blocked: ${result.path}`); }); // Test 6: Archive extraction path traversal (Zip Slip) const zipSlipAttacks = await performanceTracker.measureAsync( 'zip-slip-prevention', async () => { const maliciousEntries = [ '../../../../../../tmp/evil.sh', '../../../.bashrc', '..\\..\\..\\windows\\system32\\evil.exe', 'invoice/../../../etc/cron.d/backdoor' ]; const results = []; for (const entry of maliciousEntries) { try { const safePath = await einvoice.extractToSafePath(entry, '/tmp/safe-extract'); const isWithinBounds = safePath.startsWith('/tmp/safe-extract'); results.push({ entry, extractedTo: safePath, safe: isWithinBounds, blocked: !isWithinBounds }); } catch (error) { results.push({ entry, blocked: true, error: error.message }); } } return results; } ); zipSlipAttacks.forEach(result => { t.ok(result.safe || result.blocked, `Zip slip attack prevented: ${result.entry}`); }); // Test 7: UNC path injection (Windows) const uncPathInjection = await performanceTracker.measureAsync( 'unc-path-injection', async () => { const uncPaths = [ '\\\\attacker.com\\share\\payload.exe', '//attacker.com/share/malware', '\\\\127.0.0.1\\C$\\Windows\\System32', '\\\\?\\C:\\Windows\\System32\\drivers\\etc\\hosts' ]; const results = []; for (const uncPath of uncPaths) { try { const isUNC = await einvoice.isUNCPath(uncPath); const blocked = await einvoice.blockUNCPaths(uncPath); results.push({ path: uncPath, isUNC, blocked }); } catch (error) { results.push({ path: uncPath, blocked: true, error: error.message }); } } return results; } ); uncPathInjection.forEach(result => { if (result.isUNC) { t.ok(result.blocked, `UNC path blocked: ${result.path}`); } }); // Test 8: Special device files const deviceFiles = await performanceTracker.measureAsync( 'device-file-prevention', async () => { const devices = os.platform() === 'win32' ? ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT1', 'CON.txt', 'PRN.pdf'] : ['/dev/null', '/dev/zero', '/dev/random', '/dev/tty', '/proc/self/environ']; const results = []; for (const device of devices) { try { const isDevice = await einvoice.isDeviceFile(device); const allowed = await einvoice.allowDeviceAccess(device); results.push({ path: device, isDevice, blocked: isDevice && !allowed }); } catch (error) { results.push({ path: device, blocked: true, error: error.message }); } } return results; } ); deviceFiles.forEach(result => { if (result.isDevice) { t.ok(result.blocked, `Device file access blocked: ${result.path}`); } }); // Test 9: Mixed technique attacks const mixedAttacks = await performanceTracker.measureAsync( 'mixed-technique-attacks', async () => { const complexPaths = [ '../%2e%2e/%2e%2e/etc/passwd', '..\\..\\..%00.pdf', '/var/www/../../etc/shadow', 'C:../../../windows/system32', '\\\\?\\..\\..\\..\\windows\\system32', 'invoices/2024/../../../../../../../etc/passwd', './valid/../../invalid/../../../etc/hosts' ]; const results = []; for (const complexPath of complexPaths) { try { // Apply all security checks const normalized = await einvoice.normalizePath(complexPath); const hasTraversal = normalized.includes('..') || normalized.includes('../'); const hasNullByte = normalized.includes('\x00'); const isAbsolute = path.isAbsolute(normalized); const isUNC = normalized.startsWith('\\\\') || normalized.startsWith('//'); const safe = !hasTraversal && !hasNullByte && !isAbsolute && !isUNC; results.push({ original: complexPath, normalized, checks: { hasTraversal, hasNullByte, isAbsolute, isUNC }, safe, blocked: !safe }); } catch (error) { results.push({ original: complexPath, blocked: true, error: error.message }); } } return results; } ); mixedAttacks.forEach(result => { t.ok(result.blocked, `Mixed attack technique blocked: ${result.original}`); }); // Test 10: Real-world scenarios with invoice files const realWorldScenarios = await performanceTracker.measureAsync( 'real-world-path-scenarios', async () => { const scenarios = [ { description: 'Save invoice to uploads directory', basePath: '/var/www/uploads', userInput: 'invoice_2024_001.pdf', expected: '/var/www/uploads/invoice_2024_001.pdf' }, { description: 'Malicious filename in upload', basePath: '/var/www/uploads', userInput: '../../../etc/passwd', expected: 'blocked' }, { description: 'Extract attachment from invoice', basePath: '/tmp/attachments', userInput: 'attachment_1.xml', expected: '/tmp/attachments/attachment_1.xml' }, { description: 'Malicious attachment path', basePath: '/tmp/attachments', userInput: '../../home/user/.ssh/id_rsa', expected: 'blocked' } ]; const results = []; for (const scenario of scenarios) { try { const safePath = await einvoice.createSafePath( scenario.basePath, scenario.userInput ); const isWithinBase = safePath.startsWith(scenario.basePath); const matchesExpected = scenario.expected === 'blocked' ? !isWithinBase : safePath === scenario.expected; results.push({ description: scenario.description, result: safePath, success: matchesExpected }); } catch (error) { results.push({ description: scenario.description, result: 'blocked', success: scenario.expected === 'blocked' }); } } return results; } ); realWorldScenarios.forEach(result => { t.ok(result.success, result.description); }); // Print performance summary performanceTracker.printSummary(); }); // Run the test tap.start();