update
This commit is contained in:
481
test/suite/einvoice_security/test.sec-05.path-traversal.ts
Normal file
481
test/suite/einvoice_security/test.sec-05.path-traversal.ts
Normal 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();
|
479
test/suite/einvoice_security/test.sec-06.memory-dos.ts
Normal file
479
test/suite/einvoice_security/test.sec-06.memory-dos.ts
Normal 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();
|
480
test/suite/einvoice_security/test.sec-07.schema-security.ts
Normal file
480
test/suite/einvoice_security/test.sec-07.schema-security.ts
Normal 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();
|
487
test/suite/einvoice_security/test.sec-08.signature-validation.ts
Normal file
487
test/suite/einvoice_security/test.sec-08.signature-validation.ts
Normal 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();
|
480
test/suite/einvoice_security/test.sec-09.safe-errors.ts
Normal file
480
test/suite/einvoice_security/test.sec-09.safe-errors.ts
Normal 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();
|
682
test/suite/einvoice_security/test.sec-10.resource-limits.ts
Normal file
682
test/suite/einvoice_security/test.sec-10.resource-limits.ts
Normal 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();
|
Reference in New Issue
Block a user