479 lines
14 KiB
TypeScript
479 lines
14 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-06: Memory DoS Prevention');
|
||
|
|
||
|
tap.test('SEC-06: Memory DoS Prevention - should prevent memory exhaustion attacks', async (t) => {
|
||
|
const einvoice = new EInvoice();
|
||
|
|
||
|
// Test 1: Large attribute count attack
|
||
|
const largeAttributeAttack = await performanceTracker.measureAsync(
|
||
|
'large-attribute-count-attack',
|
||
|
async () => {
|
||
|
// Create XML with excessive attributes
|
||
|
let attributes = '';
|
||
|
const attrCount = 1000000;
|
||
|
|
||
|
for (let i = 0; i < attrCount; i++) {
|
||
|
attributes += ` attr${i}="value${i}"`;
|
||
|
}
|
||
|
|
||
|
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice ${attributes}>
|
||
|
<ID>test</ID>
|
||
|
</Invoice>`;
|
||
|
|
||
|
const startMemory = process.memoryUsage();
|
||
|
const startTime = Date.now();
|
||
|
|
||
|
try {
|
||
|
await einvoice.parseXML(maliciousXML);
|
||
|
|
||
|
const endMemory = process.memoryUsage();
|
||
|
const endTime = Date.now();
|
||
|
|
||
|
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||
|
const timeTaken = endTime - startTime;
|
||
|
|
||
|
return {
|
||
|
prevented: memoryIncrease < 100 * 1024 * 1024, // Less than 100MB
|
||
|
memoryIncrease,
|
||
|
timeTaken,
|
||
|
attributeCount: attrCount
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return {
|
||
|
prevented: true,
|
||
|
rejected: true,
|
||
|
error: error.message
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(largeAttributeAttack.prevented, 'Large attribute count attack was prevented');
|
||
|
|
||
|
// Test 2: Deep recursion attack
|
||
|
const deepRecursionAttack = await performanceTracker.measureAsync(
|
||
|
'deep-recursion-attack',
|
||
|
async () => {
|
||
|
// Create deeply nested XML
|
||
|
const depth = 50000;
|
||
|
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<Invoice>';
|
||
|
|
||
|
for (let i = 0; i < depth; i++) {
|
||
|
xml += `<Level${i}>`;
|
||
|
}
|
||
|
xml += 'data';
|
||
|
for (let i = depth - 1; i >= 0; i--) {
|
||
|
xml += `</Level${i}>`;
|
||
|
}
|
||
|
xml += '</Invoice>';
|
||
|
|
||
|
const startMemory = process.memoryUsage();
|
||
|
|
||
|
try {
|
||
|
await einvoice.parseXML(xml);
|
||
|
|
||
|
const endMemory = process.memoryUsage();
|
||
|
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
return {
|
||
|
prevented: memoryIncrease < 50 * 1024 * 1024, // Less than 50MB
|
||
|
memoryIncrease,
|
||
|
depth
|
||
|
};
|
||
|
} catch (error) {
|
||
|
// Stack overflow or depth limit is also prevention
|
||
|
return {
|
||
|
prevented: true,
|
||
|
rejected: true,
|
||
|
error: error.message
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(deepRecursionAttack.prevented, 'Deep recursion attack was prevented');
|
||
|
|
||
|
// Test 3: Large text node attack
|
||
|
const largeTextNodeAttack = await performanceTracker.measureAsync(
|
||
|
'large-text-node-attack',
|
||
|
async () => {
|
||
|
// Create XML with huge text content
|
||
|
const textSize = 500 * 1024 * 1024; // 500MB of text
|
||
|
const chunk = 'A'.repeat(1024 * 1024); // 1MB chunks
|
||
|
|
||
|
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice>
|
||
|
<Description>${chunk}</Description>
|
||
|
</Invoice>`;
|
||
|
|
||
|
const startMemory = process.memoryUsage();
|
||
|
const startTime = Date.now();
|
||
|
|
||
|
try {
|
||
|
// Simulate streaming or chunked processing
|
||
|
for (let i = 0; i < 500; i++) {
|
||
|
await einvoice.parseXML(maliciousXML);
|
||
|
|
||
|
// Check memory growth
|
||
|
const currentMemory = process.memoryUsage();
|
||
|
const memoryGrowth = currentMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
if (memoryGrowth > 200 * 1024 * 1024) {
|
||
|
throw new Error('Memory limit exceeded');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const endTime = Date.now();
|
||
|
const finalMemory = process.memoryUsage();
|
||
|
|
||
|
return {
|
||
|
prevented: false,
|
||
|
memoryGrowth: finalMemory.heapUsed - startMemory.heapUsed,
|
||
|
timeTaken: endTime - startTime
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return {
|
||
|
prevented: true,
|
||
|
limited: true,
|
||
|
error: error.message
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(largeTextNodeAttack.prevented, 'Large text node attack was prevented');
|
||
|
|
||
|
// Test 4: Namespace pollution attack
|
||
|
const namespacePollutionAttack = await performanceTracker.measureAsync(
|
||
|
'namespace-pollution-attack',
|
||
|
async () => {
|
||
|
// Create XML with excessive namespaces
|
||
|
let namespaces = '';
|
||
|
const nsCount = 100000;
|
||
|
|
||
|
for (let i = 0; i < nsCount; i++) {
|
||
|
namespaces += ` xmlns:ns${i}="http://example.com/ns${i}"`;
|
||
|
}
|
||
|
|
||
|
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice${namespaces}>
|
||
|
<ID>test</ID>
|
||
|
</Invoice>`;
|
||
|
|
||
|
const startMemory = process.memoryUsage();
|
||
|
|
||
|
try {
|
||
|
await einvoice.parseXML(maliciousXML);
|
||
|
|
||
|
const endMemory = process.memoryUsage();
|
||
|
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
return {
|
||
|
prevented: memoryIncrease < 50 * 1024 * 1024,
|
||
|
memoryIncrease,
|
||
|
namespaceCount: nsCount
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return {
|
||
|
prevented: true,
|
||
|
rejected: true
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(namespacePollutionAttack.prevented, 'Namespace pollution attack was prevented');
|
||
|
|
||
|
// Test 5: Entity expansion memory attack
|
||
|
const entityExpansionMemory = await performanceTracker.measureAsync(
|
||
|
'entity-expansion-memory-attack',
|
||
|
async () => {
|
||
|
// Create entities that expand exponentially
|
||
|
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<!DOCTYPE foo [
|
||
|
<!ENTITY base "AAAAAAAAAA">
|
||
|
<!ENTITY level1 "&base;&base;&base;&base;&base;&base;&base;&base;&base;&base;">
|
||
|
<!ENTITY level2 "&level1;&level1;&level1;&level1;&level1;&level1;&level1;&level1;&level1;&level1;">
|
||
|
<!ENTITY level3 "&level2;&level2;&level2;&level2;&level2;&level2;&level2;&level2;&level2;&level2;">
|
||
|
]>
|
||
|
<Invoice>
|
||
|
<Data>&level3;</Data>
|
||
|
</Invoice>`;
|
||
|
|
||
|
const startMemory = process.memoryUsage();
|
||
|
const memoryLimit = 100 * 1024 * 1024; // 100MB limit
|
||
|
|
||
|
try {
|
||
|
await einvoice.parseXML(maliciousXML);
|
||
|
|
||
|
const endMemory = process.memoryUsage();
|
||
|
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
return {
|
||
|
prevented: memoryIncrease < memoryLimit,
|
||
|
memoryIncrease,
|
||
|
expansionFactor: Math.pow(10, 3) // Expected expansion
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return {
|
||
|
prevented: true,
|
||
|
rejected: true,
|
||
|
error: error.message
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(entityExpansionMemory.prevented, 'Entity expansion memory attack was prevented');
|
||
|
|
||
|
// Test 6: Array allocation attack
|
||
|
const arrayAllocationAttack = await performanceTracker.measureAsync(
|
||
|
'array-allocation-attack',
|
||
|
async () => {
|
||
|
// Create XML that forces large array allocations
|
||
|
let elements = '';
|
||
|
const elementCount = 10000000;
|
||
|
|
||
|
for (let i = 0; i < elementCount; i++) {
|
||
|
elements += `<Item${i}/>`;
|
||
|
}
|
||
|
|
||
|
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice>
|
||
|
<Items>${elements}</Items>
|
||
|
</Invoice>`;
|
||
|
|
||
|
const startMemory = process.memoryUsage();
|
||
|
|
||
|
try {
|
||
|
await einvoice.parseXML(maliciousXML);
|
||
|
|
||
|
const endMemory = process.memoryUsage();
|
||
|
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
return {
|
||
|
prevented: memoryIncrease < 200 * 1024 * 1024,
|
||
|
memoryIncrease,
|
||
|
elementCount
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return {
|
||
|
prevented: true,
|
||
|
rejected: true
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(arrayAllocationAttack.prevented, 'Array allocation attack was prevented');
|
||
|
|
||
|
// Test 7: Memory leak through repeated operations
|
||
|
const memoryLeakTest = await performanceTracker.measureAsync(
|
||
|
'memory-leak-prevention',
|
||
|
async () => {
|
||
|
const iterations = 1000;
|
||
|
const samples = [];
|
||
|
|
||
|
// Force GC if available
|
||
|
if (global.gc) {
|
||
|
global.gc();
|
||
|
}
|
||
|
|
||
|
const baselineMemory = process.memoryUsage().heapUsed;
|
||
|
|
||
|
for (let i = 0; i < iterations; i++) {
|
||
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice>
|
||
|
<ID>INV-${i}</ID>
|
||
|
<Amount>${Math.random() * 1000}</Amount>
|
||
|
</Invoice>`;
|
||
|
|
||
|
await einvoice.parseXML(xml);
|
||
|
|
||
|
if (i % 100 === 0) {
|
||
|
// Sample memory every 100 iterations
|
||
|
const currentMemory = process.memoryUsage().heapUsed;
|
||
|
samples.push({
|
||
|
iteration: i,
|
||
|
memory: currentMemory - baselineMemory
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Calculate memory growth trend
|
||
|
const firstSample = samples[0];
|
||
|
const lastSample = samples[samples.length - 1];
|
||
|
const memoryGrowthRate = (lastSample.memory - firstSample.memory) / (lastSample.iteration - firstSample.iteration);
|
||
|
|
||
|
return {
|
||
|
prevented: memoryGrowthRate < 1000, // Less than 1KB per iteration
|
||
|
memoryGrowthRate,
|
||
|
totalIterations: iterations,
|
||
|
samples
|
||
|
};
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(memoryLeakTest.prevented, 'Memory leak through repeated operations was prevented');
|
||
|
|
||
|
// Test 8: Concurrent memory attacks
|
||
|
const concurrentMemoryAttack = await performanceTracker.measureAsync(
|
||
|
'concurrent-memory-attacks',
|
||
|
async () => {
|
||
|
const concurrentAttacks = 10;
|
||
|
const startMemory = process.memoryUsage();
|
||
|
|
||
|
// Create multiple large XML documents
|
||
|
const createLargeXML = (id: number) => {
|
||
|
const size = 10 * 1024 * 1024; // 10MB
|
||
|
const data = 'X'.repeat(size);
|
||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice>
|
||
|
<ID>${id}</ID>
|
||
|
<LargeData>${data}</LargeData>
|
||
|
</Invoice>`;
|
||
|
};
|
||
|
|
||
|
try {
|
||
|
// Process multiple large documents concurrently
|
||
|
const promises = [];
|
||
|
for (let i = 0; i < concurrentAttacks; i++) {
|
||
|
promises.push(einvoice.parseXML(createLargeXML(i)));
|
||
|
}
|
||
|
|
||
|
await Promise.all(promises);
|
||
|
|
||
|
const endMemory = process.memoryUsage();
|
||
|
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
return {
|
||
|
prevented: memoryIncrease < 500 * 1024 * 1024, // Less than 500MB total
|
||
|
memoryIncrease,
|
||
|
concurrentCount: concurrentAttacks
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return {
|
||
|
prevented: true,
|
||
|
rejected: true,
|
||
|
error: error.message
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(concurrentMemoryAttack.prevented, 'Concurrent memory attacks were prevented');
|
||
|
|
||
|
// Test 9: Cache pollution attack
|
||
|
const cachePollutionAttack = await performanceTracker.measureAsync(
|
||
|
'cache-pollution-attack',
|
||
|
async () => {
|
||
|
const uniqueDocuments = 10000;
|
||
|
const startMemory = process.memoryUsage();
|
||
|
|
||
|
try {
|
||
|
// Parse many unique documents to pollute cache
|
||
|
for (let i = 0; i < uniqueDocuments; i++) {
|
||
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice>
|
||
|
<UniqueID>ID-${Math.random()}-${Date.now()}-${i}</UniqueID>
|
||
|
<RandomData>${Math.random().toString(36).substring(2)}</RandomData>
|
||
|
</Invoice>`;
|
||
|
|
||
|
await einvoice.parseXML(xml);
|
||
|
|
||
|
// Check memory growth periodically
|
||
|
if (i % 1000 === 0) {
|
||
|
const currentMemory = process.memoryUsage();
|
||
|
const memoryGrowth = currentMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
if (memoryGrowth > 100 * 1024 * 1024) {
|
||
|
throw new Error('Cache memory limit exceeded');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const endMemory = process.memoryUsage();
|
||
|
const totalMemoryGrowth = endMemory.heapUsed - startMemory.heapUsed;
|
||
|
|
||
|
return {
|
||
|
prevented: totalMemoryGrowth < 100 * 1024 * 1024,
|
||
|
memoryGrowth: totalMemoryGrowth,
|
||
|
documentsProcessed: uniqueDocuments
|
||
|
};
|
||
|
} catch (error) {
|
||
|
return {
|
||
|
prevented: true,
|
||
|
limited: true,
|
||
|
error: error.message
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.ok(cachePollutionAttack.prevented, 'Cache pollution attack was prevented');
|
||
|
|
||
|
// Test 10: Memory exhaustion recovery
|
||
|
const memoryExhaustionRecovery = await performanceTracker.measureAsync(
|
||
|
'memory-exhaustion-recovery',
|
||
|
async () => {
|
||
|
const results = {
|
||
|
attacksAttempted: 0,
|
||
|
attacksPrevented: 0,
|
||
|
recovered: false
|
||
|
};
|
||
|
|
||
|
// Try various memory attacks
|
||
|
const attacks = [
|
||
|
() => 'A'.repeat(100 * 1024 * 1024), // 100MB string
|
||
|
() => new Array(10000000).fill('data'), // Large array
|
||
|
() => { const obj = {}; for(let i = 0; i < 1000000; i++) obj[`key${i}`] = i; return obj; } // Large object
|
||
|
];
|
||
|
|
||
|
for (const attack of attacks) {
|
||
|
results.attacksAttempted++;
|
||
|
|
||
|
try {
|
||
|
const payload = attack();
|
||
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice>
|
||
|
<Data>${JSON.stringify(payload).substring(0, 1000)}</Data>
|
||
|
</Invoice>`;
|
||
|
|
||
|
await einvoice.parseXML(xml);
|
||
|
} catch (error) {
|
||
|
results.attacksPrevented++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Test if system recovered and can process normal documents
|
||
|
try {
|
||
|
const normalXML = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice>
|
||
|
<ID>NORMAL-001</ID>
|
||
|
<Amount>100.00</Amount>
|
||
|
</Invoice>`;
|
||
|
|
||
|
await einvoice.parseXML(normalXML);
|
||
|
results.recovered = true;
|
||
|
} catch (error) {
|
||
|
results.recovered = false;
|
||
|
}
|
||
|
|
||
|
return results;
|
||
|
}
|
||
|
);
|
||
|
|
||
|
t.equal(memoryExhaustionRecovery.attacksPrevented, memoryExhaustionRecovery.attacksAttempted, 'All memory attacks were prevented');
|
||
|
t.ok(memoryExhaustionRecovery.recovered, 'System recovered after memory attacks');
|
||
|
|
||
|
// Print performance summary
|
||
|
performanceTracker.printSummary();
|
||
|
});
|
||
|
|
||
|
// Run the test
|
||
|
tap.start();
|