einvoice/test/suite/einvoice_security/test.sec-05.path-traversal.ts

481 lines
14 KiB
TypeScript
Raw Normal View History

2025-05-26 04:04:51 +00:00
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();