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();
|
Reference in New Issue
Block a user