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();