import { tap, expect } 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 () => { // 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.fromXml(maliciousXML); // If parsing succeeds, the entity should not be resolved // Check that no system file content appears in the invoice data const invoiceJson = JSON.stringify(result); expect(invoiceJson).not.toMatch(/root:/); expect(invoiceJson).not.toMatch(/bin\/bash/); return { prevented: true, method: 'sanitized' }; } catch (error) { // Parser should reject XXE attempts expect(error).toBeTruthy(); return { prevented: true, method: 'rejected', error: error.message }; } } ); expect(basicXXE.prevented).toBeTrue(); // Test 2: Prevent parameter entity XXE const parameterEntityXXE = await performanceTracker.measureAsync( 'parameter-entity-xxe', async () => { const maliciousXML = ` "> %eval; %exfil; ]> test `; try { await EInvoice.fromXml(maliciousXML); return { prevented: true, method: 'sanitized' }; } catch (error) { return { prevented: true, method: 'rejected', error: error.message }; } } ); expect(parameterEntityXXE.prevented).toBeTrue(); // Test 3: Prevent SSRF via XXE const ssrfXXE = await performanceTracker.measureAsync( 'ssrf-xxe-prevention', async () => { const maliciousXML = ` ]> &xxe; `; try { const result = await EInvoice.fromXml(maliciousXML); // Check that SSRF content was not retrieved // The URL should not have been resolved to actual content const invoiceJson = JSON.stringify(result); // Should not contain actual admin page content, but the URL itself is OK expect(invoiceJson).not.toMatch(/Administration Panel/); expect(invoiceJson).not.toMatch(/Dashboard/); expect(invoiceJson.length).toBeGreaterThan(100); // Should have some content return { prevented: true, method: 'sanitized' }; } catch (error) { return { prevented: true, method: 'rejected', error: error.message }; } } ); expect(ssrfXXE.prevented).toBeTrue(); // 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.fromXml(maliciousXML); const endTime = Date.now(); const endMemory = process.memoryUsage().heapUsed; // Should complete quickly without memory explosion expect(endTime - startTime).toBeLessThan(1000); expect(endMemory - startMemory).toBeLessThan(10 * 1024 * 1024); return { prevented: true, method: 'limited' }; } catch (error) { return { prevented: true, method: 'rejected', error: error.message }; } } ); expect(billionLaughs.prevented).toBeTrue(); // Test 5: Prevent external DTD loading const externalDTD = await performanceTracker.measureAsync( 'external-dtd-prevention', async () => { const maliciousXML = ` 12345 `; try { await EInvoice.fromXml(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 }; } } ); expect(externalDTD.prevented).toBeTrue(); // 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.fromXml(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 => { expect(result.prevented).toBeTrue(); if (result.method === 'sanitized') { expect(result.hasEntities).toBeFalse(); } }); // Test 7: Nested entity attacks const nestedEntities = await performanceTracker.measureAsync( 'nested-entity-prevention', async () => { const maliciousXML = ` ]> &level3; `; try { const result = await EInvoice.fromXml(maliciousXML); // Check that nested entities were not resolved const invoiceJson = JSON.stringify(result); expect(invoiceJson).not.toMatch(/root:/); return { prevented: true }; } catch (error) { return { prevented: true, error: error.message }; } } ); expect(nestedEntities.prevented).toBeTrue(); // Test 8: Unicode-based XXE attempts const unicodeXXE = await performanceTracker.measureAsync( 'unicode-xxe-prevention', async () => { const maliciousXML = ` ]> &xxe; `; try { const result = await EInvoice.fromXml(maliciousXML); // Check that Unicode-encoded entities were not resolved const invoiceJson = JSON.stringify(result); expect(invoiceJson).not.toMatch(/root:/); return { prevented: true }; } catch (error) { return { prevented: true, error: error.message }; } } ); expect(unicodeXXE.prevented).toBeTrue(); // Performance tracking complete }); // 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: EInvoice): boolean { const json = JSON.stringify(document); // Check for common system file signatures (not URLs) const signatures = [ 'root:x:0:0', // System user entries 'bin/bash', // Shell entries '/bin/sh', // Shell paths 'daemon:', // System processes 'nobody:', // System users 'shadow:', // Password files 'staff' // Group entries ]; return signatures.some(sig => json.includes(sig)); } // Run the test tap.start();