This commit is contained in:
2025-05-26 04:04:51 +00:00
parent 39942638d9
commit 1d52ce1211
23 changed files with 13545 additions and 4 deletions

View File

@ -0,0 +1,481 @@
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';
import * as os from 'os';
const performanceTracker = new PerformanceTracker('SEC-05: Path Traversal Prevention');
tap.test('SEC-05: Path Traversal Prevention - should prevent directory traversal attacks', async (t) => {
const einvoice = new EInvoice();
// Test 1: Basic path traversal attempts
const basicPathTraversal = await performanceTracker.measureAsync(
'basic-path-traversal-prevention',
async () => {
const traversalPaths = [
'../../../etc/passwd',
'..\\..\\..\\windows\\system32\\config\\sam',
'../../../../../../../../etc/shadow',
'./../.../.././../etc/hosts',
'..%2F..%2F..%2Fetc%2Fpasswd',
'..%252f..%252f..%252fetc%252fpasswd'
];
const results = [];
for (const maliciousPath of traversalPaths) {
try {
// Test file read operation
const canRead = await einvoice.canAccessFile(maliciousPath);
// Test file write operation
const canWrite = await einvoice.canWriteFile(maliciousPath);
// Test path resolution
const resolvedPath = await einvoice.resolvePath(maliciousPath);
results.push({
path: maliciousPath,
blocked: !canRead && !canWrite,
resolved: resolvedPath,
containsTraversal: resolvedPath?.includes('..') || false
});
} catch (error) {
results.push({
path: maliciousPath,
blocked: true,
error: error.message
});
}
}
return results;
}
);
basicPathTraversal.forEach(result => {
t.ok(result.blocked, `Path traversal blocked: ${result.path}`);
t.notOk(result.containsTraversal, 'Resolved path does not contain traversal sequences');
});
// Test 2: Unicode and encoding bypass attempts
const encodingBypass = await performanceTracker.measureAsync(
'encoding-bypass-attempts',
async () => {
const encodedPaths = [
'..%c0%af..%c0%afetc%c0%afpasswd', // Overlong UTF-8
'..%25c0%25af..%25c0%25afetc%25c0%25afpasswd', // Double encoding
'..%c1%9c..%c1%9cetc%c1%9cpasswd', // Invalid UTF-8
'\u002e\u002e/\u002e\u002e/etc/passwd', // Unicode dots
'..%u002f..%u002fetc%u002fpasswd', // IIS Unicode
'..%255c..%255c..%255cwindows%255csystem32' // Double encoded backslash
];
const results = [];
for (const encodedPath of encodedPaths) {
try {
const normalized = await einvoice.normalizePath(encodedPath);
const isSafe = await einvoice.isPathSafe(normalized);
results.push({
original: encodedPath,
normalized,
safe: isSafe,
blocked: !isSafe
});
} catch (error) {
results.push({
original: encodedPath,
blocked: true,
error: error.message
});
}
}
return results;
}
);
encodingBypass.forEach(result => {
t.ok(result.blocked || !result.safe, `Encoded path traversal blocked: ${result.original.substring(0, 30)}...`);
});
// Test 3: Null byte injection
const nullByteInjection = await performanceTracker.measureAsync(
'null-byte-injection',
async () => {
const nullBytePaths = [
'invoice.pdf\x00.txt',
'report.xml\x00.exe',
'document\x00../../../etc/passwd',
'file.pdf%00.jsp',
'data\u0000../../../../sensitive.dat'
];
const results = [];
for (const nullPath of nullBytePaths) {
try {
const cleaned = await einvoice.cleanPath(nullPath);
const hasNullByte = cleaned.includes('\x00') || cleaned.includes('%00');
results.push({
original: nullPath.replace(/\x00/g, '\\x00'),
cleaned,
nullByteRemoved: !hasNullByte,
safe: !hasNullByte && !cleaned.includes('..')
});
} catch (error) {
results.push({
original: nullPath.replace(/\x00/g, '\\x00'),
blocked: true,
error: error.message
});
}
}
return results;
}
);
nullByteInjection.forEach(result => {
t.ok(result.nullByteRemoved || result.blocked, `Null byte injection prevented: ${result.original}`);
});
// Test 4: Symbolic link attacks
const symlinkAttacks = await performanceTracker.measureAsync(
'symlink-attack-prevention',
async () => {
const symlinkPaths = [
'/tmp/invoice_link -> /etc/passwd',
'C:\\temp\\report.lnk',
'./uploads/../../sensitive/data',
'invoices/current -> /home/user/.ssh/id_rsa'
];
const results = [];
for (const linkPath of symlinkPaths) {
try {
const isSymlink = await einvoice.detectSymlink(linkPath);
const followsSymlinks = await einvoice.followsSymlinks();
results.push({
path: linkPath,
isSymlink,
followsSymlinks,
safe: !isSymlink || !followsSymlinks
});
} catch (error) {
results.push({
path: linkPath,
safe: true,
error: error.message
});
}
}
return results;
}
);
symlinkAttacks.forEach(result => {
t.ok(result.safe, `Symlink attack prevented: ${result.path}`);
});
// Test 5: Absolute path injection
const absolutePathInjection = await performanceTracker.measureAsync(
'absolute-path-injection',
async () => {
const absolutePaths = [
'/etc/passwd',
'C:\\Windows\\System32\\config\\SAM',
'\\\\server\\share\\sensitive.dat',
'file:///etc/shadow',
os.platform() === 'win32' ? 'C:\\Users\\Admin\\Documents' : '/home/user/.ssh/'
];
const results = [];
for (const absPath of absolutePaths) {
try {
const isAllowed = await einvoice.isAbsolutePathAllowed(absPath);
const normalized = await einvoice.normalizeToSafePath(absPath);
results.push({
path: absPath,
allowed: isAllowed,
normalized,
blocked: !isAllowed
});
} catch (error) {
results.push({
path: absPath,
blocked: true,
error: error.message
});
}
}
return results;
}
);
absolutePathInjection.forEach(result => {
t.ok(result.blocked, `Absolute path injection blocked: ${result.path}`);
});
// Test 6: Archive extraction path traversal (Zip Slip)
const zipSlipAttacks = await performanceTracker.measureAsync(
'zip-slip-prevention',
async () => {
const maliciousEntries = [
'../../../../../../tmp/evil.sh',
'../../../.bashrc',
'..\\..\\..\\windows\\system32\\evil.exe',
'invoice/../../../etc/cron.d/backdoor'
];
const results = [];
for (const entry of maliciousEntries) {
try {
const safePath = await einvoice.extractToSafePath(entry, '/tmp/safe-extract');
const isWithinBounds = safePath.startsWith('/tmp/safe-extract');
results.push({
entry,
extractedTo: safePath,
safe: isWithinBounds,
blocked: !isWithinBounds
});
} catch (error) {
results.push({
entry,
blocked: true,
error: error.message
});
}
}
return results;
}
);
zipSlipAttacks.forEach(result => {
t.ok(result.safe || result.blocked, `Zip slip attack prevented: ${result.entry}`);
});
// Test 7: UNC path injection (Windows)
const uncPathInjection = await performanceTracker.measureAsync(
'unc-path-injection',
async () => {
const uncPaths = [
'\\\\attacker.com\\share\\payload.exe',
'//attacker.com/share/malware',
'\\\\127.0.0.1\\C$\\Windows\\System32',
'\\\\?\\C:\\Windows\\System32\\drivers\\etc\\hosts'
];
const results = [];
for (const uncPath of uncPaths) {
try {
const isUNC = await einvoice.isUNCPath(uncPath);
const blocked = await einvoice.blockUNCPaths(uncPath);
results.push({
path: uncPath,
isUNC,
blocked
});
} catch (error) {
results.push({
path: uncPath,
blocked: true,
error: error.message
});
}
}
return results;
}
);
uncPathInjection.forEach(result => {
if (result.isUNC) {
t.ok(result.blocked, `UNC path blocked: ${result.path}`);
}
});
// Test 8: Special device files
const deviceFiles = await performanceTracker.measureAsync(
'device-file-prevention',
async () => {
const devices = os.platform() === 'win32'
? ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT1', 'CON.txt', 'PRN.pdf']
: ['/dev/null', '/dev/zero', '/dev/random', '/dev/tty', '/proc/self/environ'];
const results = [];
for (const device of devices) {
try {
const isDevice = await einvoice.isDeviceFile(device);
const allowed = await einvoice.allowDeviceAccess(device);
results.push({
path: device,
isDevice,
blocked: isDevice && !allowed
});
} catch (error) {
results.push({
path: device,
blocked: true,
error: error.message
});
}
}
return results;
}
);
deviceFiles.forEach(result => {
if (result.isDevice) {
t.ok(result.blocked, `Device file access blocked: ${result.path}`);
}
});
// Test 9: Mixed technique attacks
const mixedAttacks = await performanceTracker.measureAsync(
'mixed-technique-attacks',
async () => {
const complexPaths = [
'../%2e%2e/%2e%2e/etc/passwd',
'..\\..\\..%00.pdf',
'/var/www/../../etc/shadow',
'C:../../../windows/system32',
'\\\\?\\..\\..\\..\\windows\\system32',
'invoices/2024/../../../../../../../etc/passwd',
'./valid/../../invalid/../../../etc/hosts'
];
const results = [];
for (const complexPath of complexPaths) {
try {
// Apply all security checks
const normalized = await einvoice.normalizePath(complexPath);
const hasTraversal = normalized.includes('..') || normalized.includes('../');
const hasNullByte = normalized.includes('\x00');
const isAbsolute = path.isAbsolute(normalized);
const isUNC = normalized.startsWith('\\\\') || normalized.startsWith('//');
const safe = !hasTraversal && !hasNullByte && !isAbsolute && !isUNC;
results.push({
original: complexPath,
normalized,
checks: {
hasTraversal,
hasNullByte,
isAbsolute,
isUNC
},
safe,
blocked: !safe
});
} catch (error) {
results.push({
original: complexPath,
blocked: true,
error: error.message
});
}
}
return results;
}
);
mixedAttacks.forEach(result => {
t.ok(result.blocked, `Mixed attack technique blocked: ${result.original}`);
});
// Test 10: Real-world scenarios with invoice files
const realWorldScenarios = await performanceTracker.measureAsync(
'real-world-path-scenarios',
async () => {
const scenarios = [
{
description: 'Save invoice to uploads directory',
basePath: '/var/www/uploads',
userInput: 'invoice_2024_001.pdf',
expected: '/var/www/uploads/invoice_2024_001.pdf'
},
{
description: 'Malicious filename in upload',
basePath: '/var/www/uploads',
userInput: '../../../etc/passwd',
expected: 'blocked'
},
{
description: 'Extract attachment from invoice',
basePath: '/tmp/attachments',
userInput: 'attachment_1.xml',
expected: '/tmp/attachments/attachment_1.xml'
},
{
description: 'Malicious attachment path',
basePath: '/tmp/attachments',
userInput: '../../home/user/.ssh/id_rsa',
expected: 'blocked'
}
];
const results = [];
for (const scenario of scenarios) {
try {
const safePath = await einvoice.createSafePath(
scenario.basePath,
scenario.userInput
);
const isWithinBase = safePath.startsWith(scenario.basePath);
const matchesExpected = scenario.expected === 'blocked'
? !isWithinBase
: safePath === scenario.expected;
results.push({
description: scenario.description,
result: safePath,
success: matchesExpected
});
} catch (error) {
results.push({
description: scenario.description,
result: 'blocked',
success: scenario.expected === 'blocked'
});
}
}
return results;
}
);
realWorldScenarios.forEach(result => {
t.ok(result.success, result.description);
});
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();

View File

@ -0,0 +1,479 @@
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();

View File

@ -0,0 +1,480 @@
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-07: Schema Validation Security');
tap.test('SEC-07: Schema Validation Security - should securely handle schema validation', async (t) => {
const einvoice = new EInvoice();
// Test 1: Malicious schema location
const maliciousSchemaLocation = await performanceTracker.measureAsync(
'malicious-schema-location',
async () => {
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://malicious.com/steal-data.xsd">
<ID>TEST-001</ID>
</Invoice>`;
try {
const result = await einvoice.validateWithSchema(maliciousXML);
return {
blocked: !result?.valid || result?.schemaBlocked,
schemaURL: 'http://malicious.com/steal-data.xsd',
message: 'External schema should be blocked'
};
} catch (error) {
return {
blocked: true,
error: error.message
};
}
}
);
t.ok(maliciousSchemaLocation.blocked, 'Malicious schema location was blocked');
// Test 2: Schema with external entity references
const schemaWithExternalEntities = await performanceTracker.measureAsync(
'schema-external-entities',
async () => {
const xmlWithExternalSchema = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE schema [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<Invoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="invoice.xsd">
<ID>&xxe;</ID>
</Invoice>`;
try {
const result = await einvoice.validateWithSchema(xmlWithExternalSchema);
return {
blocked: !result?.valid || !result?.content?.includes('root:'),
hasXXE: result?.content?.includes('root:') || false
};
} catch (error) {
return {
blocked: true,
error: error.message
};
}
}
);
t.ok(schemaWithExternalEntities.blocked, 'Schema with external entities was blocked');
t.notOk(schemaWithExternalEntities.hasXXE, 'XXE content was not resolved');
// Test 3: Recursive schema imports
const recursiveSchemaImports = await performanceTracker.measureAsync(
'recursive-schema-imports',
async () => {
const xmlWithRecursiveSchema = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="schema1.xsd">
<!-- schema1.xsd imports schema2.xsd which imports schema1.xsd -->
<ID>TEST-001</ID>
</Invoice>`;
const startTime = Date.now();
const maxTime = 5000; // 5 seconds max
try {
const result = await einvoice.validateWithSchema(xmlWithRecursiveSchema);
const timeTaken = Date.now() - startTime;
return {
prevented: timeTaken < maxTime,
timeTaken,
valid: result?.valid || false
};
} catch (error) {
return {
prevented: true,
error: error.message
};
}
}
);
t.ok(recursiveSchemaImports.prevented, 'Recursive schema imports were prevented');
// Test 4: Schema complexity attacks
const schemaComplexityAttack = await performanceTracker.measureAsync(
'schema-complexity-attack',
async () => {
// Create XML with complex nested structure that exploits schema validation
let complexContent = '<Items>';
for (let i = 0; i < 1000; i++) {
complexContent += '<Item>';
for (let j = 0; j < 100; j++) {
complexContent += `<SubItem${j} attr1="val" attr2="val" attr3="val"/>`;
}
complexContent += '</Item>';
}
complexContent += '</Items>';
const complexXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
${complexContent}
</Invoice>`;
const startTime = Date.now();
const startMemory = process.memoryUsage();
try {
await einvoice.validateWithSchema(complexXML);
const endTime = Date.now();
const endMemory = process.memoryUsage();
const timeTaken = endTime - startTime;
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
return {
prevented: timeTaken < 10000 && memoryIncrease < 100 * 1024 * 1024,
timeTaken,
memoryIncrease
};
} catch (error) {
return {
prevented: true,
error: error.message
};
}
}
);
t.ok(schemaComplexityAttack.prevented, 'Schema complexity attack was prevented');
// Test 5: Schema with malicious regular expressions
const maliciousRegexSchema = await performanceTracker.measureAsync(
'malicious-regex-schema',
async () => {
// XML that would trigger ReDoS if schema uses vulnerable regex
const maliciousInput = 'a'.repeat(100) + '!';
const xmlWithMaliciousContent = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<Email>${maliciousInput}@example.com</Email>
<Phone>${maliciousInput}</Phone>
</Invoice>`;
const startTime = Date.now();
try {
await einvoice.validateWithSchema(xmlWithMaliciousContent);
const timeTaken = Date.now() - startTime;
return {
prevented: timeTaken < 1000, // Should complete quickly
timeTaken,
inputLength: maliciousInput.length
};
} catch (error) {
return {
prevented: true,
error: error.message
};
}
}
);
t.ok(maliciousRegexSchema.prevented, 'Malicious regex in schema was handled safely');
// Test 6: Schema URL injection
const schemaURLInjection = await performanceTracker.measureAsync(
'schema-url-injection',
async () => {
const injectionAttempts = [
'http://example.com/schema.xsd?file=/etc/passwd',
'http://example.com/schema.xsd#../../admin/schema.xsd',
'http://example.com/schema.xsd%00.malicious',
'javascript:alert("XSS")',
'file:///etc/passwd'
];
const results = [];
for (const schemaURL of injectionAttempts) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="${schemaURL}">
<ID>TEST</ID>
</Invoice>`;
try {
const result = await einvoice.validateWithSchema(xml);
results.push({
url: schemaURL,
blocked: !result?.valid || result?.schemaBlocked,
allowed: result?.valid && !result?.schemaBlocked
});
} catch (error) {
results.push({
url: schemaURL,
blocked: true,
error: error.message
});
}
}
return results;
}
);
schemaURLInjection.forEach(result => {
t.ok(result.blocked, `Schema URL injection blocked: ${result.url}`);
});
// Test 7: Schema include/import security
const schemaIncludeSecurity = await performanceTracker.measureAsync(
'schema-include-security',
async () => {
// Test schema that tries to include external resources
const xmlWithIncludes = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- Schema tries to include external files -->
<ID>TEST-001</ID>
</Invoice>`;
const testCases = [
{ type: 'local-file', path: '../../../etc/passwd' },
{ type: 'remote-url', path: 'http://evil.com/malicious.xsd' },
{ type: 'relative-path', path: '../../../../sensitive/data.xsd' }
];
const results = [];
for (const testCase of testCases) {
try {
const result = await einvoice.validateSchemaIncludes(xmlWithIncludes, testCase.path);
results.push({
type: testCase.type,
blocked: !result?.allowed,
path: testCase.path
});
} catch (error) {
results.push({
type: testCase.type,
blocked: true,
error: error.message
});
}
}
return results;
}
);
schemaIncludeSecurity.forEach(result => {
t.ok(result.blocked, `Schema include blocked: ${result.type}`);
});
// Test 8: Schema validation bypass attempts
const schemaBypassAttempts = await performanceTracker.measureAsync(
'schema-validation-bypass',
async () => {
const bypassAttempts = [
{
name: 'namespace-confusion',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="fake-namespace" xmlns:real="actual-namespace">
<ID>BYPASS-001</ID>
<real:MaliciousData>attack-payload</real:MaliciousData>
</Invoice>`
},
{
name: 'schema-version-mismatch',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice version="99.99">
<ID>BYPASS-002</ID>
<UnsupportedElement>should-not-validate</UnsupportedElement>
</Invoice>`
},
{
name: 'encoding-trick',
xml: `<?xml version="1.0" encoding="UTF-16"?>
<Invoice>
<ID>BYPASS-003</ID>
<HiddenData>malicious</HiddenData>
</Invoice>`
}
];
const results = [];
for (const attempt of bypassAttempts) {
try {
const result = await einvoice.validateWithSchema(attempt.xml);
results.push({
name: attempt.name,
valid: result?.valid || false,
caught: !result?.valid || result?.hasWarnings
});
} catch (error) {
results.push({
name: attempt.name,
caught: true,
error: error.message
});
}
}
return results;
}
);
schemaBypassAttempts.forEach(result => {
t.ok(result.caught, `Schema bypass attempt caught: ${result.name}`);
});
// Test 9: Schema caching security
const schemaCachingSecurity = await performanceTracker.measureAsync(
'schema-caching-security',
async () => {
const results = {
cachePoison: false,
cacheBypass: false,
cacheOverflow: false
};
// Test 1: Cache poisoning
try {
// First, load legitimate schema
await einvoice.loadSchema('legitimate.xsd');
// Try to poison cache with malicious version
await einvoice.loadSchema('legitimate.xsd', {
content: '<malicious>content</malicious>',
forceReload: false
});
// Check if cache was poisoned
const cachedSchema = await einvoice.getSchemaFromCache('legitimate.xsd');
results.cachePoison = cachedSchema?.includes('malicious') || false;
} catch (error) {
// Error is good - means poisoning was prevented
}
// Test 2: Cache bypass
try {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xsi:schemaLocation="cached-schema.xsd?nocache=${Date.now()}">
<ID>TEST</ID>
</Invoice>`;
const result1 = await einvoice.validateWithSchema(xml);
const result2 = await einvoice.validateWithSchema(xml);
// Should use cache, not fetch twice
results.cacheBypass = result1?.cacheHit === false && result2?.cacheHit === true;
} catch (error) {
// Expected
}
// Test 3: Cache overflow
try {
// Try to overflow cache with many schemas
for (let i = 0; i < 10000; i++) {
await einvoice.loadSchema(`schema-${i}.xsd`);
}
// Check memory usage
const memUsage = process.memoryUsage();
results.cacheOverflow = memUsage.heapUsed > 500 * 1024 * 1024; // 500MB
} catch (error) {
// Expected - cache should have limits
}
return results;
}
);
t.notOk(schemaCachingSecurity.cachePoison, 'Cache poisoning was prevented');
t.notOk(schemaCachingSecurity.cacheOverflow, 'Cache overflow was prevented');
// Test 10: Real-world schema validation
const realWorldSchemaValidation = await performanceTracker.measureAsync(
'real-world-schema-validation',
async () => {
const formats = ['ubl', 'cii', 'zugferd'];
const results = [];
for (const format of formats) {
try {
// Create a valid invoice for the format
const invoice = createTestInvoice(format);
// Validate with proper schema
const validationResult = await einvoice.validateWithSchema(invoice, {
format,
strict: true,
securityChecks: true
});
results.push({
format,
valid: validationResult?.valid || false,
secure: validationResult?.securityPassed || false,
errors: validationResult?.errors || []
});
} catch (error) {
results.push({
format,
valid: false,
secure: false,
error: error.message
});
}
}
return results;
}
);
realWorldSchemaValidation.forEach(result => {
t.ok(result.secure, `${result.format} schema validation is secure`);
});
// Print performance summary
performanceTracker.printSummary();
});
// Helper function to create test invoices
function createTestInvoice(format: string): string {
const invoices = {
ubl: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<UBLVersionID>2.1</UBLVersionID>
<ID>INV-001</ID>
<IssueDate>2024-01-15</IssueDate>
</Invoice>`,
cii: `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocument>
<ram:ID>INV-001</ram:ID>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`,
zugferd: `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017:compliant:factur-x.eu:1p0:basic</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
</rsm:CrossIndustryInvoice>`
};
return invoices[format] || invoices.ubl;
}
// Run the test
tap.start();

View File

@ -0,0 +1,487 @@
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-08: Cryptographic Signature Validation');
tap.test('SEC-08: Cryptographic Signature Validation - should securely validate digital signatures', async (t) => {
const einvoice = new EInvoice();
// Test 1: Valid signature verification
const validSignatureVerification = await performanceTracker.measureAsync(
'valid-signature-verification',
async () => {
// Create a mock signed invoice
const signedInvoice = createSignedInvoice({
id: 'INV-001',
amount: 1000.00,
validSignature: true
});
try {
const result = await einvoice.verifySignature(signedInvoice);
return {
valid: result?.signatureValid || false,
signerInfo: result?.signerInfo || {},
certificateChain: result?.certificateChain || [],
timestamp: result?.timestamp
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
);
t.ok(validSignatureVerification.valid, 'Valid signature was verified successfully');
// Test 2: Invalid signature detection
const invalidSignatureDetection = await performanceTracker.measureAsync(
'invalid-signature-detection',
async () => {
// Create invoice with tampered signature
const tamperedInvoice = createSignedInvoice({
id: 'INV-002',
amount: 2000.00,
validSignature: false,
tampered: true
});
try {
const result = await einvoice.verifySignature(tamperedInvoice);
return {
valid: result?.signatureValid || false,
reason: result?.invalidReason,
tamperedFields: result?.tamperedFields || []
};
} catch (error) {
return {
valid: false,
rejected: true,
error: error.message
};
}
}
);
t.notOk(invalidSignatureDetection.valid, 'Invalid signature was detected');
// Test 3: Certificate chain validation
const certificateChainValidation = await performanceTracker.measureAsync(
'certificate-chain-validation',
async () => {
const testCases = [
{ type: 'valid-chain', valid: true },
{ type: 'self-signed', valid: false },
{ type: 'expired-cert', valid: false },
{ type: 'revoked-cert', valid: false },
{ type: 'untrusted-ca', valid: false }
];
const results = [];
for (const testCase of testCases) {
const invoice = createSignedInvoice({
id: `INV-${testCase.type}`,
certificateType: testCase.type
});
try {
const result = await einvoice.verifyCertificateChain(invoice);
results.push({
type: testCase.type,
expectedValid: testCase.valid,
actualValid: result?.chainValid || false,
trustPath: result?.trustPath || []
});
} catch (error) {
results.push({
type: testCase.type,
expectedValid: testCase.valid,
actualValid: false,
error: error.message
});
}
}
return results;
}
);
certificateChainValidation.forEach(result => {
t.equal(result.actualValid, result.expectedValid,
`Certificate chain ${result.type}: expected ${result.expectedValid}, got ${result.actualValid}`);
});
// Test 4: Timestamp validation
const timestampValidation = await performanceTracker.measureAsync(
'timestamp-validation',
async () => {
const timestampTests = [
{ type: 'valid-timestamp', time: new Date(), valid: true },
{ type: 'future-timestamp', time: new Date(Date.now() + 86400000), valid: false },
{ type: 'expired-timestamp', time: new Date('2020-01-01'), valid: false },
{ type: 'no-timestamp', time: null, valid: false }
];
const results = [];
for (const test of timestampTests) {
const invoice = createSignedInvoice({
id: `INV-TS-${test.type}`,
timestamp: test.time
});
try {
const result = await einvoice.verifyTimestamp(invoice);
results.push({
type: test.type,
valid: result?.timestampValid || false,
time: result?.timestamp,
trusted: result?.timestampTrusted || false
});
} catch (error) {
results.push({
type: test.type,
valid: false,
error: error.message
});
}
}
return results;
}
);
timestampValidation.forEach(result => {
const expected = timestampTests.find(t => t.type === result.type)?.valid;
t.equal(result.valid, expected, `Timestamp ${result.type} validation`);
});
// Test 5: Algorithm security verification
const algorithmSecurity = await performanceTracker.measureAsync(
'algorithm-security-verification',
async () => {
const algorithms = [
{ name: 'RSA-SHA256', secure: true },
{ name: 'RSA-SHA1', secure: false }, // Deprecated
{ name: 'MD5', secure: false }, // Insecure
{ name: 'RSA-SHA512', secure: true },
{ name: 'ECDSA-SHA256', secure: true },
{ name: 'DSA-SHA1', secure: false } // Weak
];
const results = [];
for (const algo of algorithms) {
const invoice = createSignedInvoice({
id: `INV-ALGO-${algo.name}`,
algorithm: algo.name
});
try {
const result = await einvoice.verifySignatureAlgorithm(invoice);
results.push({
algorithm: algo.name,
expectedSecure: algo.secure,
actualSecure: result?.algorithmSecure || false,
strength: result?.algorithmStrength
});
} catch (error) {
results.push({
algorithm: algo.name,
expectedSecure: algo.secure,
actualSecure: false,
error: error.message
});
}
}
return results;
}
);
algorithmSecurity.forEach(result => {
t.equal(result.actualSecure, result.expectedSecure,
`Algorithm ${result.algorithm} security check`);
});
// Test 6: Multiple signature handling
const multipleSignatures = await performanceTracker.measureAsync(
'multiple-signature-handling',
async () => {
const invoice = createMultiplySignedInvoice({
id: 'INV-MULTI-001',
signatures: [
{ signer: 'Issuer', valid: true },
{ signer: 'Approval1', valid: true },
{ signer: 'Approval2', valid: false },
{ signer: 'Final', valid: true }
]
});
try {
const result = await einvoice.verifyAllSignatures(invoice);
return {
totalSignatures: result?.signatures?.length || 0,
validSignatures: result?.signatures?.filter(s => s.valid)?.length || 0,
invalidSignatures: result?.signatures?.filter(s => !s.valid) || [],
allValid: result?.allValid || false
};
} catch (error) {
return {
error: error.message
};
}
}
);
t.equal(multipleSignatures.totalSignatures, 4, 'All signatures were processed');
t.equal(multipleSignatures.validSignatures, 3, 'Valid signatures were counted correctly');
t.notOk(multipleSignatures.allValid, 'Overall validation failed due to invalid signature');
// Test 7: Signature stripping attacks
const signatureStrippingAttack = await performanceTracker.measureAsync(
'signature-stripping-attack',
async () => {
const originalInvoice = createSignedInvoice({
id: 'INV-STRIP-001',
amount: 1000.00,
validSignature: true
});
// Attempt to strip signature
const strippedInvoice = originalInvoice.replace(/<ds:Signature.*?<\/ds:Signature>/gs, '');
try {
const result = await einvoice.detectSignatureStripping(strippedInvoice, {
requireSignature: true
});
return {
detected: result?.signatureRequired && !result?.signaturePresent,
hasSignature: result?.signaturePresent || false,
stripped: result?.possiblyStripped || false
};
} catch (error) {
return {
detected: true,
error: error.message
};
}
}
);
t.ok(signatureStrippingAttack.detected, 'Signature stripping was detected');
// Test 8: XML signature wrapping attacks
const signatureWrappingAttack = await performanceTracker.measureAsync(
'signature-wrapping-attack',
async () => {
// Create invoice with wrapped signature attack
const wrappedInvoice = createWrappedSignatureAttack({
originalId: 'INV-001',
originalAmount: 100.00,
wrappedId: 'INV-EVIL',
wrappedAmount: 10000.00
});
try {
const result = await einvoice.detectSignatureWrapping(wrappedInvoice);
return {
detected: result?.wrappingDetected || false,
multipleRoots: result?.multipleRoots || false,
signatureScope: result?.signatureScope,
validStructure: result?.validXMLStructure || false
};
} catch (error) {
return {
detected: true,
error: error.message
};
}
}
);
t.ok(signatureWrappingAttack.detected, 'Signature wrapping attack was detected');
// Test 9: Key strength validation
const keyStrengthValidation = await performanceTracker.measureAsync(
'key-strength-validation',
async () => {
const keyTests = [
{ type: 'RSA-1024', bits: 1024, secure: false },
{ type: 'RSA-2048', bits: 2048, secure: true },
{ type: 'RSA-4096', bits: 4096, secure: true },
{ type: 'ECDSA-256', bits: 256, secure: true },
{ type: 'DSA-1024', bits: 1024, secure: false }
];
const results = [];
for (const test of keyTests) {
const invoice = createSignedInvoice({
id: `INV-KEY-${test.type}`,
keyType: test.type,
keyBits: test.bits
});
try {
const result = await einvoice.validateKeyStrength(invoice);
results.push({
type: test.type,
bits: test.bits,
expectedSecure: test.secure,
actualSecure: result?.keySecure || false,
recommendation: result?.recommendation
});
} catch (error) {
results.push({
type: test.type,
actualSecure: false,
error: error.message
});
}
}
return results;
}
);
keyStrengthValidation.forEach(result => {
t.equal(result.actualSecure, result.expectedSecure,
`Key strength ${result.type} validation`);
});
// Test 10: Real-world PDF signature validation
const pdfSignatureValidation = await performanceTracker.measureAsync(
'pdf-signature-validation',
async () => {
const results = {
signedPDFs: 0,
validSignatures: 0,
invalidSignatures: 0,
unsignedPDFs: 0
};
// Test with sample PDFs (in real implementation, would use corpus)
const testPDFs = [
{ name: 'signed-valid.pdf', signed: true, valid: true },
{ name: 'signed-tampered.pdf', signed: true, valid: false },
{ name: 'unsigned.pdf', signed: false, valid: null }
];
for (const pdf of testPDFs) {
try {
const result = await einvoice.verifyPDFSignature(pdf.name);
if (!result?.hasSiganture) {
results.unsignedPDFs++;
} else {
results.signedPDFs++;
if (result?.signatureValid) {
results.validSignatures++;
} else {
results.invalidSignatures++;
}
}
} catch (error) {
// Count as invalid if verification fails
if (pdf.signed) {
results.invalidSignatures++;
}
}
}
return results;
}
);
t.equal(pdfSignatureValidation.signedPDFs, 2, 'Detected all signed PDFs');
t.equal(pdfSignatureValidation.validSignatures, 1, 'Valid signatures verified correctly');
t.equal(pdfSignatureValidation.invalidSignatures, 1, 'Invalid signatures detected correctly');
// Print performance summary
performanceTracker.printSummary();
});
// Helper function to create signed invoice
function createSignedInvoice(options: any): string {
const { id, amount, validSignature = true, algorithm = 'RSA-SHA256',
timestamp = new Date(), certificateType = 'valid-chain',
keyType = 'RSA-2048', keyBits = 2048, tampered = false } = options;
const invoiceData = `<Invoice><ID>${id}</ID><Amount>${amount || 100}</Amount></Invoice>`;
const signature = validSignature && !tampered ?
`<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:SignatureMethod Algorithm="${algorithm}"/>
</ds:SignedInfo>
<ds:SignatureValue>VALID_SIGNATURE_VALUE</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>CERTIFICATE_${certificateType}</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>` :
`<ds:Signature>INVALID_SIGNATURE</ds:Signature>`;
return `<?xml version="1.0" encoding="UTF-8"?>${invoiceData}${signature}`;
}
// Helper function to create multiply signed invoice
function createMultiplySignedInvoice(options: any): string {
const { id, signatures } = options;
let signatureXML = '';
for (const sig of signatures) {
signatureXML += `<ds:Signature id="${sig.signer}">
<ds:SignatureValue>${sig.valid ? 'VALID' : 'INVALID'}_SIG_${sig.signer}</ds:SignatureValue>
</ds:Signature>`;
}
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>${id}</ID>
${signatureXML}
</Invoice>`;
}
// Helper function to create wrapped signature attack
function createWrappedSignatureAttack(options: any): string {
const { originalId, originalAmount, wrappedId, wrappedAmount } = options;
return `<?xml version="1.0" encoding="UTF-8"?>
<Wrapper>
<Invoice>
<ID>${wrappedId}</ID>
<Amount>${wrappedAmount}</Amount>
</Invoice>
<OriginalInvoice>
<Invoice>
<ID>${originalId}</ID>
<Amount>${originalAmount}</Amount>
</Invoice>
<ds:Signature>
<!-- Signature only covers OriginalInvoice -->
<ds:Reference URI="#original">
<ds:DigestValue>VALID_DIGEST</ds:DigestValue>
</ds:Reference>
</ds:Signature>
</OriginalInvoice>
</Wrapper>`;
}
// Run the test
tap.start();

View File

@ -0,0 +1,480 @@
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-09: Safe Error Messages');
tap.test('SEC-09: Safe Error Messages - should provide secure error messages without leaking sensitive information', async (t) => {
const einvoice = new EInvoice();
// Test 1: File path disclosure prevention
const filePathDisclosure = await performanceTracker.measureAsync(
'file-path-disclosure-prevention',
async () => {
const sensitiveFiles = [
'/home/user/invoices/secret/invoice.xml',
'C:\\Users\\Admin\\Documents\\Confidential\\invoice.pdf',
'/var/www/private/customer-data.xml',
'../../../../../../etc/passwd'
];
const results = [];
for (const filePath of sensitiveFiles) {
try {
// Attempt to read non-existent file
await einvoice.readFile(filePath);
} catch (error) {
const errorMsg = error.message || error.toString();
results.push({
originalPath: filePath,
errorMessage: errorMsg,
leaksPath: errorMsg.includes(filePath) ||
errorMsg.includes('/home/') ||
errorMsg.includes('C:\\') ||
errorMsg.includes('/var/'),
leaksUsername: errorMsg.includes('user') ||
errorMsg.includes('Admin'),
leaksSystemInfo: errorMsg.includes('Linux') ||
errorMsg.includes('Windows') ||
errorMsg.includes('Darwin')
});
}
}
return results;
}
);
filePathDisclosure.forEach(result => {
t.notOk(result.leaksPath, 'Error does not leak file path');
t.notOk(result.leaksUsername, 'Error does not leak username');
t.notOk(result.leaksSystemInfo, 'Error does not leak system info');
});
// Test 2: Database error message sanitization
const databaseErrorSanitization = await performanceTracker.measureAsync(
'database-error-sanitization',
async () => {
const dbErrors = [
{
type: 'connection',
original: 'Connection failed to database server at 192.168.1.100:5432 with user "admin"',
expected: 'Database connection failed'
},
{
type: 'query',
original: 'ERROR: relation "invoices" does not exist at character 15',
expected: 'Database query failed'
},
{
type: 'auth',
original: 'FATAL: password authentication failed for user "invoice_user"',
expected: 'Database authentication failed'
},
{
type: 'schema',
original: 'ERROR: column "credit_card_number" of relation "customers" does not exist',
expected: 'Database operation failed'
}
];
const results = [];
for (const dbError of dbErrors) {
try {
// Simulate database operation that would throw error
const sanitized = await einvoice.sanitizeDatabaseError(dbError.original);
results.push({
type: dbError.type,
sanitized: sanitized,
leaksIP: sanitized.includes('192.168') || sanitized.includes(':5432'),
leaksSchema: sanitized.includes('invoices') || sanitized.includes('customers'),
leaksCredentials: sanitized.includes('admin') || sanitized.includes('invoice_user'),
leaksColumns: sanitized.includes('credit_card_number')
});
} catch (error) {
results.push({
type: dbError.type,
error: error.message
});
}
}
return results;
}
);
databaseErrorSanitization.forEach(result => {
t.notOk(result.leaksIP, `${result.type}: Does not leak IP addresses`);
t.notOk(result.leaksSchema, `${result.type}: Does not leak schema names`);
t.notOk(result.leaksCredentials, `${result.type}: Does not leak credentials`);
t.notOk(result.leaksColumns, `${result.type}: Does not leak column names`);
});
// Test 3: XML parsing error sanitization
const xmlParsingErrorSanitization = await performanceTracker.measureAsync(
'xml-parsing-error-sanitization',
async () => {
const xmlErrors = [
{
xml: '<Invoice><Amount>not-a-number</Amount></Invoice>',
errorType: 'validation'
},
{
xml: '<Invoice><CreditCard>4111111111111111</CreditCard></Invoice>',
errorType: 'sensitive-data'
},
{
xml: '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><Invoice>&xxe;</Invoice>',
errorType: 'xxe-attempt'
},
{
xml: '<Invoice xmlns:hack="javascript:alert(1)"><hack:script/></Invoice>',
errorType: 'xss-attempt'
}
];
const results = [];
for (const test of xmlErrors) {
try {
await einvoice.parseXML(test.xml);
} catch (error) {
const errorMsg = error.message;
results.push({
errorType: test.errorType,
errorMessage: errorMsg,
leaksSensitiveData: errorMsg.includes('4111111111111111'),
leaksSystemPaths: errorMsg.includes('/etc/passwd') || errorMsg.includes('file:///'),
leaksAttackVector: errorMsg.includes('javascript:') || errorMsg.includes('<!ENTITY'),
providesHint: errorMsg.includes('XXE') || errorMsg.includes('external entity')
});
}
}
return results;
}
);
xmlParsingErrorSanitization.forEach(result => {
t.notOk(result.leaksSensitiveData, `${result.errorType}: Does not leak sensitive data`);
t.notOk(result.leaksSystemPaths, `${result.errorType}: Does not leak system paths`);
t.notOk(result.leaksAttackVector, `${result.errorType}: Does not leak attack details`);
});
// Test 4: Stack trace sanitization
const stackTraceSanitization = await performanceTracker.measureAsync(
'stack-trace-sanitization',
async () => {
const operations = [
{ type: 'parse-error', fn: () => einvoice.parseXML('<invalid>') },
{ type: 'validation-error', fn: () => einvoice.validate({}) },
{ type: 'conversion-error', fn: () => einvoice.convert(null, 'ubl') },
{ type: 'file-error', fn: () => einvoice.readFile('/nonexistent') }
];
const results = [];
for (const op of operations) {
try {
await op.fn();
} catch (error) {
const fullError = error.stack || error.toString();
const userError = await einvoice.getUserFriendlyError(error);
results.push({
type: op.type,
originalHasStack: fullError.includes('at '),
userErrorHasStack: userError.includes('at '),
leaksInternalPaths: userError.includes('/src/') ||
userError.includes('/node_modules/') ||
userError.includes('\\src\\'),
leaksFunctionNames: userError.includes('parseXML') ||
userError.includes('validateSchema') ||
userError.includes('convertFormat'),
leaksLineNumbers: /:\d+:\d+/.test(userError)
});
}
}
return results;
}
);
stackTraceSanitization.forEach(result => {
t.notOk(result.userErrorHasStack, `${result.type}: User error has no stack trace`);
t.notOk(result.leaksInternalPaths, `${result.type}: Does not leak internal paths`);
t.notOk(result.leaksFunctionNames, `${result.type}: Does not leak function names`);
t.notOk(result.leaksLineNumbers, `${result.type}: Does not leak line numbers`);
});
// Test 5: API key and credential scrubbing
const credentialScrubbing = await performanceTracker.measureAsync(
'credential-scrubbing',
async () => {
const errorScenarios = [
{
error: 'API call failed with key: sk_live_abc123def456',
type: 'api-key'
},
{
error: 'Authentication failed for Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
type: 'jwt-token'
},
{
error: 'Database connection string: mongodb://user:password123@localhost:27017/db',
type: 'connection-string'
},
{
error: 'AWS credentials invalid: AKIAIOSFODNN7EXAMPLE',
type: 'aws-key'
}
];
const results = [];
for (const scenario of errorScenarios) {
const scrubbed = await einvoice.scrubSensitiveData(scenario.error);
results.push({
type: scenario.type,
original: scenario.error,
scrubbed: scrubbed,
containsKey: scrubbed.includes('sk_live_') || scrubbed.includes('AKIA'),
containsPassword: scrubbed.includes('password123'),
containsToken: scrubbed.includes('eyJ'),
properlyMasked: scrubbed.includes('***') || scrubbed.includes('[REDACTED]')
});
}
return results;
}
);
credentialScrubbing.forEach(result => {
t.notOk(result.containsKey, `${result.type}: API keys are scrubbed`);
t.notOk(result.containsPassword, `${result.type}: Passwords are scrubbed`);
t.notOk(result.containsToken, `${result.type}: Tokens are scrubbed`);
t.ok(result.properlyMasked, `${result.type}: Sensitive data is properly masked`);
});
// Test 6: Version and framework disclosure
const versionDisclosure = await performanceTracker.measureAsync(
'version-framework-disclosure',
async () => {
const errors = [];
// Collect various error messages
const operations = [
() => einvoice.parseXML('<invalid>'),
() => einvoice.validateFormat('unknown'),
() => einvoice.convertFormat({}, 'invalid'),
() => einvoice.readFile('/nonexistent')
];
for (const op of operations) {
try {
await op();
} catch (error) {
errors.push(error.message || error.toString());
}
}
const results = {
errors: errors.length,
leaksNodeVersion: errors.some(e => e.includes('v14.') || e.includes('v16.') || e.includes('v18.')),
leaksFramework: errors.some(e => e.includes('Express') || e.includes('Fastify') || e.includes('NestJS')),
leaksLibraryVersion: errors.some(e => e.includes('@fin.cx/einvoice@') || e.includes('version')),
leaksXMLParser: errors.some(e => e.includes('libxml') || e.includes('sax') || e.includes('xmldom')),
leaksOS: errors.some(e => e.includes('Linux') || e.includes('Darwin') || e.includes('Windows NT'))
};
return results;
}
);
t.notOk(versionDisclosure.leaksNodeVersion, 'Does not leak Node.js version');
t.notOk(versionDisclosure.leaksFramework, 'Does not leak framework information');
t.notOk(versionDisclosure.leaksLibraryVersion, 'Does not leak library version');
t.notOk(versionDisclosure.leaksXMLParser, 'Does not leak XML parser details');
t.notOk(versionDisclosure.leaksOS, 'Does not leak operating system');
// Test 7: Timing attack prevention in errors
const timingAttackPrevention = await performanceTracker.measureAsync(
'timing-attack-prevention',
async () => {
const validationTests = [
{ id: 'VALID-001', valid: true },
{ id: 'INVALID-AT-START', valid: false },
{ id: 'INVALID-AT-END-OF-VERY-LONG-ID', valid: false }
];
const timings = [];
for (const test of validationTests) {
const iterations = 100;
const times = [];
for (let i = 0; i < iterations; i++) {
const start = process.hrtime.bigint();
try {
await einvoice.validateInvoiceId(test.id);
} catch (error) {
// Expected for invalid IDs
}
const end = process.hrtime.bigint();
times.push(Number(end - start) / 1000000); // Convert to ms
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const variance = times.reduce((sum, time) => sum + Math.pow(time - avgTime, 2), 0) / times.length;
timings.push({
id: test.id,
valid: test.valid,
avgTime,
variance,
stdDev: Math.sqrt(variance)
});
}
// Check if timing differences are significant
const validTiming = timings.find(t => t.valid);
const invalidTimings = timings.filter(t => !t.valid);
const timingDifferences = invalidTimings.map(t => ({
id: t.id,
difference: Math.abs(t.avgTime - validTiming.avgTime),
significantDifference: Math.abs(t.avgTime - validTiming.avgTime) > validTiming.stdDev * 3
}));
return {
timings,
differences: timingDifferences,
constantTime: !timingDifferences.some(d => d.significantDifference)
};
}
);
t.ok(timingAttackPrevention.constantTime, 'Error responses have constant timing');
// Test 8: Error aggregation and rate limiting info
const errorAggregation = await performanceTracker.measureAsync(
'error-aggregation-rate-limiting',
async () => {
const results = {
individualErrors: [],
aggregatedError: null,
leaksPatterns: false
};
// Generate multiple errors
for (let i = 0; i < 10; i++) {
try {
await einvoice.parseXML(`<Invalid${i}>`);
} catch (error) {
results.individualErrors.push(error.message);
}
}
// Check if errors reveal patterns
const uniqueErrors = new Set(results.individualErrors);
results.leaksPatterns = uniqueErrors.size > 5; // Too many unique errors might reveal internals
// Test aggregated error response
try {
await einvoice.batchProcess([
'<Invalid1>',
'<Invalid2>',
'<Invalid3>'
]);
} catch (error) {
results.aggregatedError = error.message;
}
return results;
}
);
t.notOk(errorAggregation.leaksPatterns, 'Errors do not reveal internal patterns');
t.ok(errorAggregation.aggregatedError, 'Batch operations provide aggregated errors');
// Test 9: Internationalization of error messages
const errorInternationalization = await performanceTracker.measureAsync(
'error-internationalization',
async () => {
const locales = ['en', 'de', 'fr', 'es', 'it'];
const results = [];
for (const locale of locales) {
try {
await einvoice.parseXML('<Invalid>', { locale });
} catch (error) {
const errorMsg = error.message;
results.push({
locale,
message: errorMsg,
isLocalized: !errorMsg.includes('Invalid XML'), // Should not be raw English
containsTechnicalTerms: /XML|parser|schema|validation/i.test(errorMsg),
userFriendly: !/:|\bat\b|\.js|\\|\//.test(errorMsg) // No technical indicators
});
}
}
return results;
}
);
errorInternationalization.forEach(result => {
t.ok(result.userFriendly, `${result.locale}: Error message is user-friendly`);
});
// Test 10: Error logging vs user display
const errorLoggingVsDisplay = await performanceTracker.measureAsync(
'error-logging-vs-display',
async () => {
let loggedError = null;
let displayedError = null;
// Mock logger to capture logged error
const originalLog = console.error;
console.error = (error) => { loggedError = error; };
try {
await einvoice.parseXML('<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><x>&xxe;</x>');
} catch (error) {
displayedError = error.message;
}
console.error = originalLog;
return {
loggedError: loggedError?.toString() || '',
displayedError: displayedError || '',
logContainsDetails: loggedError?.includes('XXE') || loggedError?.includes('entity'),
displayIsGeneric: !displayedError.includes('XXE') && !displayedError.includes('/etc/passwd'),
logHasStackTrace: loggedError?.includes('at '),
displayHasStackTrace: displayedError.includes('at ')
};
}
);
t.ok(errorLoggingVsDisplay.logContainsDetails, 'Logged error contains technical details');
t.ok(errorLoggingVsDisplay.displayIsGeneric, 'Displayed error is generic and safe');
t.notOk(errorLoggingVsDisplay.displayHasStackTrace, 'Displayed error has no stack trace');
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();

View File

@ -0,0 +1,682 @@
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 os from 'os';
const performanceTracker = new PerformanceTracker('SEC-10: Resource Limits');
tap.test('SEC-10: Resource Limits - should enforce resource consumption limits', async (t) => {
const einvoice = new EInvoice();
// Test 1: File size limits
const fileSizeLimits = await performanceTracker.measureAsync(
'file-size-limits',
async () => {
const testSizes = [
{ size: 1 * 1024 * 1024, name: '1MB', shouldPass: true },
{ size: 10 * 1024 * 1024, name: '10MB', shouldPass: true },
{ size: 50 * 1024 * 1024, name: '50MB', shouldPass: true },
{ size: 100 * 1024 * 1024, name: '100MB', shouldPass: false },
{ size: 500 * 1024 * 1024, name: '500MB', shouldPass: false }
];
const results = [];
for (const test of testSizes) {
// Create large XML content
const chunk = '<Item>'.padEnd(1024, 'X') + '</Item>'; // ~1KB per item
const itemCount = Math.floor(test.size / 1024);
let largeXML = '<?xml version="1.0" encoding="UTF-8"?><Invoice><Items>';
// Build in chunks to avoid memory issues
for (let i = 0; i < itemCount; i += 1000) {
const batchSize = Math.min(1000, itemCount - i);
largeXML += chunk.repeat(batchSize);
}
largeXML += '</Items></Invoice>';
try {
const startTime = Date.now();
const result = await einvoice.parseXML(largeXML, { maxSize: 50 * 1024 * 1024 });
const timeTaken = Date.now() - startTime;
results.push({
size: test.name,
passed: true,
expectedPass: test.shouldPass,
timeTaken,
actualSize: largeXML.length
});
} catch (error) {
results.push({
size: test.name,
passed: false,
expectedPass: test.shouldPass,
error: error.message,
actualSize: largeXML.length
});
}
}
return results;
}
);
fileSizeLimits.forEach(result => {
if (result.expectedPass) {
t.ok(result.passed, `File size ${result.size} should be accepted`);
} else {
t.notOk(result.passed, `File size ${result.size} should be rejected`);
}
});
// Test 2: Memory usage limits
const memoryUsageLimits = await performanceTracker.measureAsync(
'memory-usage-limits',
async () => {
const baselineMemory = process.memoryUsage().heapUsed;
const maxMemoryIncrease = 200 * 1024 * 1024; // 200MB limit
const operations = [
{
name: 'large-attribute-count',
fn: async () => {
let attrs = '';
for (let i = 0; i < 1000000; i++) {
attrs += ` attr${i}="value"`;
}
return `<Invoice ${attrs}></Invoice>`;
}
},
{
name: 'deep-nesting',
fn: async () => {
let xml = '';
for (let i = 0; i < 10000; i++) {
xml += `<Level${i}>`;
}
xml += 'data';
for (let i = 9999; i >= 0; i--) {
xml += `</Level${i}>`;
}
return xml;
}
},
{
name: 'large-text-nodes',
fn: async () => {
const largeText = 'A'.repeat(50 * 1024 * 1024); // 50MB
return `<Invoice><Description>${largeText}</Description></Invoice>`;
}
}
];
const results = [];
for (const op of operations) {
try {
const xml = await op.fn();
const startMemory = process.memoryUsage().heapUsed;
await einvoice.parseXML(xml, { maxMemory: maxMemoryIncrease });
const endMemory = process.memoryUsage().heapUsed;
const memoryIncrease = endMemory - startMemory;
results.push({
operation: op.name,
memoryIncrease,
withinLimit: memoryIncrease < maxMemoryIncrease,
limitExceeded: false
});
} catch (error) {
results.push({
operation: op.name,
limitExceeded: true,
error: error.message
});
}
// Force garbage collection if available
if (global.gc) {
global.gc();
}
}
return results;
}
);
memoryUsageLimits.forEach(result => {
t.ok(result.withinLimit || result.limitExceeded,
`Memory limits enforced for ${result.operation}`);
});
// Test 3: CPU time limits
const cpuTimeLimits = await performanceTracker.measureAsync(
'cpu-time-limits',
async () => {
const maxCPUTime = 5000; // 5 seconds
const cpuIntensiveOps = [
{
name: 'complex-xpath',
xml: generateComplexXML(1000),
xpath: '//Item[position() mod 2 = 0 and @id > 500]'
},
{
name: 'regex-validation',
xml: '<Invoice><Email>' + 'a'.repeat(10000) + '@example.com</Email></Invoice>',
pattern: /^([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}){1,100}$/
},
{
name: 'recursive-calculation',
xml: generateNestedCalculations(100)
}
];
const results = [];
for (const op of cpuIntensiveOps) {
const startTime = Date.now();
const startCPU = process.cpuUsage();
try {
const result = await einvoice.processWithTimeout(op, maxCPUTime);
const endTime = Date.now();
const endCPU = process.cpuUsage(startCPU);
const wallTime = endTime - startTime;
const cpuTime = (endCPU.user + endCPU.system) / 1000; // Convert to ms
results.push({
operation: op.name,
wallTime,
cpuTime,
withinLimit: wallTime < maxCPUTime,
completed: true
});
} catch (error) {
results.push({
operation: op.name,
completed: false,
timeout: error.message.includes('timeout'),
error: error.message
});
}
}
return results;
}
);
cpuTimeLimits.forEach(result => {
t.ok(result.withinLimit || result.timeout,
`CPU time limits enforced for ${result.operation}`);
});
// Test 4: Concurrent request limits
const concurrentRequestLimits = await performanceTracker.measureAsync(
'concurrent-request-limits',
async () => {
const maxConcurrent = 10;
const totalRequests = 50;
let activeRequests = 0;
let maxActiveRequests = 0;
let rejected = 0;
let completed = 0;
const makeRequest = async (id: number) => {
try {
activeRequests++;
maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
const result = await einvoice.processWithConcurrencyLimit(
`<Invoice><ID>REQ-${id}</ID></Invoice>`,
{ maxConcurrent }
);
completed++;
return { id, success: true };
} catch (error) {
if (error.message.includes('concurrent')) {
rejected++;
}
return { id, success: false, error: error.message };
} finally {
activeRequests--;
}
};
// Launch all requests concurrently
const promises = [];
for (let i = 0; i < totalRequests; i++) {
promises.push(makeRequest(i));
}
const results = await Promise.all(promises);
return {
totalRequests,
completed,
rejected,
maxActiveRequests,
maxConcurrentRespected: maxActiveRequests <= maxConcurrent,
successRate: completed / totalRequests
};
}
);
t.ok(concurrentRequestLimits.maxConcurrentRespected,
'Concurrent request limit was respected');
t.ok(concurrentRequestLimits.rejected > 0,
'Excess concurrent requests were rejected');
// Test 5: Rate limiting
const rateLimiting = await performanceTracker.measureAsync(
'rate-limiting',
async () => {
const rateLimit = 10; // 10 requests per second
const testDuration = 3000; // 3 seconds
const expectedMax = (rateLimit * testDuration / 1000) + 2; // Allow small buffer
let processed = 0;
let rejected = 0;
const startTime = Date.now();
while (Date.now() - startTime < testDuration) {
try {
await einvoice.processWithRateLimit(
'<Invoice><ID>RATE-TEST</ID></Invoice>',
{ requestsPerSecond: rateLimit }
);
processed++;
} catch (error) {
if (error.message.includes('rate limit')) {
rejected++;
}
}
// Small delay to prevent tight loop
await new Promise(resolve => setTimeout(resolve, 10));
}
const actualRate = processed / (testDuration / 1000);
return {
processed,
rejected,
duration: testDuration,
actualRate,
targetRate: rateLimit,
withinLimit: processed <= expectedMax
};
}
);
t.ok(rateLimiting.withinLimit, 'Rate limiting is enforced');
t.ok(rateLimiting.rejected > 0, 'Excess requests were rate limited');
// Test 6: Nested entity limits
const nestedEntityLimits = await performanceTracker.measureAsync(
'nested-entity-limits',
async () => {
const entityDepths = [10, 50, 100, 500, 1000];
const maxDepth = 100;
const results = [];
for (const depth of entityDepths) {
// Create nested entities
let entityDef = '<!DOCTYPE foo [\n';
let entityValue = 'base';
for (let i = 0; i < depth; i++) {
entityDef += ` <!ENTITY level${i} "${entityValue}">\n`;
entityValue = `&level${i};`;
}
entityDef += ']>';
const xml = `<?xml version="1.0" encoding="UTF-8"?>
${entityDef}
<Invoice>
<Data>${entityValue}</Data>
</Invoice>`;
try {
await einvoice.parseXML(xml, { maxEntityDepth: maxDepth });
results.push({
depth,
allowed: true,
withinLimit: depth <= maxDepth
});
} catch (error) {
results.push({
depth,
allowed: false,
withinLimit: depth <= maxDepth,
error: error.message
});
}
}
return results;
}
);
nestedEntityLimits.forEach(result => {
if (result.withinLimit) {
t.ok(result.allowed, `Entity depth ${result.depth} should be allowed`);
} else {
t.notOk(result.allowed, `Entity depth ${result.depth} should be rejected`);
}
});
// Test 7: Output size limits
const outputSizeLimits = await performanceTracker.measureAsync(
'output-size-limits',
async () => {
const testCases = [
{
name: 'normal-output',
itemCount: 100,
shouldPass: true
},
{
name: 'large-output',
itemCount: 10000,
shouldPass: true
},
{
name: 'excessive-output',
itemCount: 1000000,
shouldPass: false
}
];
const maxOutputSize = 100 * 1024 * 1024; // 100MB
const results = [];
for (const test of testCases) {
const invoice = {
id: 'OUTPUT-TEST',
items: Array(test.itemCount).fill(null).map((_, i) => ({
id: `ITEM-${i}`,
description: 'Test item with some description text',
amount: Math.random() * 1000
}))
};
try {
const output = await einvoice.convertToXML(invoice, {
maxOutputSize
});
results.push({
name: test.name,
itemCount: test.itemCount,
outputSize: output.length,
passed: true,
expectedPass: test.shouldPass
});
} catch (error) {
results.push({
name: test.name,
itemCount: test.itemCount,
passed: false,
expectedPass: test.shouldPass,
error: error.message
});
}
}
return results;
}
);
outputSizeLimits.forEach(result => {
if (result.expectedPass) {
t.ok(result.passed, `Output ${result.name} should be allowed`);
} else {
t.notOk(result.passed, `Output ${result.name} should be limited`);
}
});
// Test 8: Timeout enforcement
const timeoutEnforcement = await performanceTracker.measureAsync(
'timeout-enforcement',
async () => {
const timeoutTests = [
{
name: 'quick-operation',
delay: 100,
timeout: 1000,
shouldComplete: true
},
{
name: 'slow-operation',
delay: 2000,
timeout: 1000,
shouldComplete: false
},
{
name: 'infinite-loop-protection',
delay: Infinity,
timeout: 500,
shouldComplete: false
}
];
const results = [];
for (const test of timeoutTests) {
const startTime = Date.now();
try {
await einvoice.processWithTimeout(async () => {
if (test.delay === Infinity) {
// Simulate infinite loop
while (true) {
// Busy wait
}
} else {
await new Promise(resolve => setTimeout(resolve, test.delay));
}
return 'completed';
}, test.timeout);
const duration = Date.now() - startTime;
results.push({
name: test.name,
completed: true,
duration,
withinTimeout: duration < test.timeout + 100 // Small buffer
});
} catch (error) {
const duration = Date.now() - startTime;
results.push({
name: test.name,
completed: false,
duration,
timedOut: error.message.includes('timeout'),
expectedTimeout: !test.shouldComplete
});
}
}
return results;
}
);
timeoutEnforcement.forEach(result => {
if (result.expectedTimeout !== undefined) {
t.equal(result.timedOut, result.expectedTimeout,
`Timeout enforcement for ${result.name}`);
}
});
// Test 9: Connection pool limits
const connectionPoolLimits = await performanceTracker.measureAsync(
'connection-pool-limits',
async () => {
const maxConnections = 5;
const totalRequests = 20;
const connectionStats = {
created: 0,
reused: 0,
rejected: 0,
activeConnections: new Set()
};
const requests = [];
for (let i = 0; i < totalRequests; i++) {
const request = einvoice.fetchWithConnectionPool(
`https://example.com/invoice/${i}`,
{
maxConnections,
onConnect: (id) => {
connectionStats.created++;
connectionStats.activeConnections.add(id);
},
onReuse: () => {
connectionStats.reused++;
},
onReject: () => {
connectionStats.rejected++;
},
onClose: (id) => {
connectionStats.activeConnections.delete(id);
}
}
).catch(error => ({ error: error.message }));
requests.push(request);
}
await Promise.all(requests);
return {
maxConnections,
totalRequests,
connectionsCreated: connectionStats.created,
connectionsReused: connectionStats.reused,
requestsRejected: connectionStats.rejected,
maxActiveReached: connectionStats.created <= maxConnections
};
}
);
t.ok(connectionPoolLimits.maxActiveReached,
'Connection pool limit was respected');
// Test 10: Resource cleanup verification
const resourceCleanup = await performanceTracker.measureAsync(
'resource-cleanup-verification',
async () => {
const initialResources = {
memory: process.memoryUsage(),
handles: process._getActiveHandles?.()?.length || 0,
requests: process._getActiveRequests?.()?.length || 0
};
// Perform various operations that consume resources
const operations = [
() => einvoice.parseXML('<Invoice>' + 'A'.repeat(1000000) + '</Invoice>'),
() => einvoice.validateSchema('<Invoice></Invoice>'),
() => einvoice.convertFormat({ id: 'TEST' }, 'ubl'),
() => einvoice.processLargeFile('test.xml', { streaming: true })
];
// Execute operations
for (const op of operations) {
try {
await op();
} catch (error) {
// Expected for some operations
}
}
// Force cleanup
await einvoice.cleanup();
// Force GC if available
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const finalResources = {
memory: process.memoryUsage(),
handles: process._getActiveHandles?.()?.length || 0,
requests: process._getActiveRequests?.()?.length || 0
};
const memoryLeaked = finalResources.memory.heapUsed - initialResources.memory.heapUsed > 10 * 1024 * 1024; // 10MB threshold
const handlesLeaked = finalResources.handles > initialResources.handles + 2; // Allow small variance
const requestsLeaked = finalResources.requests > initialResources.requests;
return {
memoryBefore: initialResources.memory.heapUsed,
memoryAfter: finalResources.memory.heapUsed,
memoryDiff: finalResources.memory.heapUsed - initialResources.memory.heapUsed,
handlesBefore: initialResources.handles,
handlesAfter: finalResources.handles,
requestsBefore: initialResources.requests,
requestsAfter: finalResources.requests,
properCleanup: !memoryLeaked && !handlesLeaked && !requestsLeaked
};
}
);
t.ok(resourceCleanup.properCleanup, 'Resources were properly cleaned up');
// Print performance summary
performanceTracker.printSummary();
});
// Helper function to generate complex XML
function generateComplexXML(itemCount: number): string {
let xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice><Items>';
for (let i = 0; i < itemCount; i++) {
xml += `<Item id="${i}" category="cat${i % 10}" price="${Math.random() * 1000}">
<Name>Item ${i}</Name>
<Description>Description for item ${i}</Description>
</Item>`;
}
xml += '</Items></Invoice>';
return xml;
}
// Helper function to generate nested calculations
function generateNestedCalculations(depth: number): string {
let xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice>';
for (let i = 0; i < depth; i++) {
xml += `<Calculation level="${i}">
<Value>${Math.random() * 100}</Value>
<Operation>multiply</Operation>`;
}
xml += '<Result>1</Result>';
for (let i = depth - 1; i >= 0; i--) {
xml += '</Calculation>';
}
xml += '</Invoice>';
return xml;
}
// Run the test
tap.start();