454 lines
12 KiB
TypeScript
454 lines
12 KiB
TypeScript
|
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();
|