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'; const performanceTracker = new PerformanceTracker('SEC-01: XXE Prevention'); tap.test('SEC-01: XML External Entity (XXE) Prevention - should prevent XXE attacks', async (t) => { const einvoice = new EInvoice(); // Test 1: Prevent basic XXE attack with external entity const basicXXE = await performanceTracker.measureAsync( 'basic-xxe-prevention', async () => { const maliciousXML = ` ]> &xxe; `; try { // Should either throw or sanitize the XXE attempt const result = await einvoice.parseXML(maliciousXML); // If parsing succeeds, the entity should not be resolved if (result && result.InvoiceNumber) { const content = result.InvoiceNumber.toString(); t.notMatch(content, /root:/, 'XXE entity should not resolve to file contents'); t.notMatch(content, /bin\/bash/, 'XXE entity should not contain system file data'); } return { prevented: true, method: 'sanitized' }; } catch (error) { // Parser should reject XXE attempts t.ok(error, 'Parser correctly rejected XXE attempt'); return { prevented: true, method: 'rejected', error: error.message }; } } ); t.ok(basicXXE.prevented, 'Basic XXE attack was prevented'); // Test 2: Prevent parameter entity XXE const parameterEntityXXE = await performanceTracker.measureAsync( 'parameter-entity-xxe', async () => { const maliciousXML = ` "> %eval; %exfil; ]> test `; try { await einvoice.parseXML(maliciousXML); return { prevented: true, method: 'sanitized' }; } catch (error) { return { prevented: true, method: 'rejected', error: error.message }; } } ); t.ok(parameterEntityXXE.prevented, 'Parameter entity XXE was prevented'); // Test 3: Prevent SSRF via XXE const ssrfXXE = await performanceTracker.measureAsync( 'ssrf-xxe-prevention', async () => { const maliciousXML = ` ]> &xxe; `; try { const result = await einvoice.parseXML(maliciousXML); if (result && result.Description) { const content = result.Description.toString(); t.notMatch(content, /admin/, 'SSRF content should not be retrieved'); t.notEqual(content.length, 0, 'Entity should be handled but not resolved'); } return { prevented: true, method: 'sanitized' }; } catch (error) { return { prevented: true, method: 'rejected', error: error.message }; } } ); t.ok(ssrfXXE.prevented, 'SSRF via XXE was prevented'); // Test 4: Prevent billion laughs attack (XML bomb) const billionLaughs = await performanceTracker.measureAsync( 'billion-laughs-prevention', async () => { const maliciousXML = ` ]> &lol4; `; const startTime = Date.now(); const startMemory = process.memoryUsage().heapUsed; try { await einvoice.parseXML(maliciousXML); const endTime = Date.now(); const endMemory = process.memoryUsage().heapUsed; // Should complete quickly without memory explosion t.ok(endTime - startTime < 1000, 'Parsing completed within time limit'); t.ok(endMemory - startMemory < 10 * 1024 * 1024, 'Memory usage stayed reasonable'); return { prevented: true, method: 'limited' }; } catch (error) { return { prevented: true, method: 'rejected', error: error.message }; } } ); t.ok(billionLaughs.prevented, 'Billion laughs attack was prevented'); // Test 5: Prevent external DTD loading const externalDTD = await performanceTracker.measureAsync( 'external-dtd-prevention', async () => { const maliciousXML = ` 12345 `; try { await einvoice.parseXML(maliciousXML); // If parsing succeeds, DTD should not have been loaded return { prevented: true, method: 'ignored' }; } catch (error) { return { prevented: true, method: 'rejected', error: error.message }; } } ); t.ok(externalDTD.prevented, 'External DTD loading was prevented'); // Test 6: Test with real invoice formats const realFormatTests = await performanceTracker.measureAsync( 'real-format-xxe-tests', async () => { const formats = ['ubl', 'cii']; const results = []; for (const format of formats) { // Create a malicious invoice in each format const maliciousInvoice = createMaliciousInvoice(format); try { const result = await einvoice.parseDocument(maliciousInvoice); results.push({ format, prevented: true, method: 'sanitized', hasEntities: checkForResolvedEntities(result) }); } catch (error) { results.push({ format, prevented: true, method: 'rejected', error: error.message }); } } return results; } ); realFormatTests.forEach(result => { t.ok(result.prevented, `XXE prevented in ${result.format} format`); if (result.method === 'sanitized') { t.notOk(result.hasEntities, `No resolved entities in ${result.format}`); } }); // Test 7: Nested entity attacks const nestedEntities = await performanceTracker.measureAsync( 'nested-entity-prevention', async () => { const maliciousXML = ` ]> &level3; `; try { const result = await einvoice.parseXML(maliciousXML); if (result && result.Note) { const content = result.Note.toString(); t.notMatch(content, /root:/, 'Nested entities should not resolve'); } return { prevented: true }; } catch (error) { return { prevented: true, error: error.message }; } } ); t.ok(nestedEntities.prevented, 'Nested entity attack was prevented'); // Test 8: Unicode-based XXE attempts const unicodeXXE = await performanceTracker.measureAsync( 'unicode-xxe-prevention', async () => { const maliciousXML = ` ]> &xxe; `; try { const result = await einvoice.parseXML(maliciousXML); if (result && result.Data) { const content = result.Data.toString(); t.notMatch(content, /root:/, 'Unicode-encoded XXE should not resolve'); } return { prevented: true }; } catch (error) { return { prevented: true, error: error.message }; } } ); t.ok(unicodeXXE.prevented, 'Unicode-based XXE was prevented'); // Print performance summary performanceTracker.printSummary(); }); // Helper function to create malicious invoices in different formats function createMaliciousInvoice(format: string): string { const xxePayload = ` ]>`; if (format === 'ubl') { return ` ${xxePayload} &xxe; 2024-01-01 `; } else if (format === 'cii') { return ` ${xxePayload} &xxe; `; } return ''; } // Helper function to check if any entities were resolved function checkForResolvedEntities(document: any): boolean { const json = JSON.stringify(document); // Check for common system file signatures const signatures = [ 'root:', 'bin/bash', '/etc/', 'localhost', 'admin', 'passwd', 'shadow', '127.0.0.1' ]; return signatures.some(sig => json.includes(sig)); } // Run the test tap.start();