@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
210 lines • 7.93 kB
JavaScript
/**
* Path Traversal Prevention
* Provides comprehensive protection against path traversal attacks
*/
import * as path from 'path';
export class PathTraversalPrevention {
static DANGEROUS_PATTERNS = [
// Directory traversal patterns
/\.\./g,
/\.\.\/|\.\.\\/,
/\.\.\//g,
/\.\.\\/g,
// URL encoded traversal
/%2e%2e%2f/gi,
/%2e%2e%5c/gi,
/%2e%2e/gi,
// Double URL encoded
/%252e%252e%252f/gi,
/%252e%252e%255c/gi,
// Unicode encoded
/\u002e\u002e\u002f/g,
/\u002e\u002e\u005c/g,
// Null byte injection
/\x00/g,
/%00/gi,
// Absolute path attempts
/^\/|^[a-zA-Z]:\\/,
// UNC path attempts (Windows)
/^\\\\|^\\\\/,
// Stream access (Windows)
/:/g
];
static DANGEROUS_EXTENSIONS = [
'.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar',
'.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl', '.sh', '.bash'
];
static SYSTEM_DIRECTORIES = [
'windows', 'system32', 'program files', 'programdata',
'etc', 'bin', 'sbin', 'usr', 'var', 'tmp', 'boot',
'proc', 'sys', 'dev', 'root'
];
/**
* Validates a file path for traversal attacks
*/
static validatePath(inputPath, options = {}) {
const errors = [];
if (!inputPath || typeof inputPath !== 'string') {
errors.push('Path must be a non-empty string');
return { isValid: false, errors };
}
// Check for dangerous patterns
for (const pattern of this.DANGEROUS_PATTERNS) {
if (pattern.test(inputPath)) {
errors.push(`Dangerous path pattern detected: ${pattern.source}`);
}
}
// Check for null bytes and control characters
if (/[\x00-\x1f\x7f-\x9f]/.test(inputPath)) {
errors.push('Path contains control characters or null bytes');
}
// Check path length
if (inputPath.length > 260) { // Windows MAX_PATH limit
errors.push('Path exceeds maximum allowed length');
}
// Normalize and resolve the path
let normalizedPath;
try {
normalizedPath = path.normalize(inputPath);
// Check if normalization revealed traversal attempts
if (normalizedPath.includes('..')) {
errors.push('Path contains directory traversal after normalization');
}
}
catch (error) {
errors.push('Path normalization failed');
return { isValid: false, errors };
}
// Check absolute vs relative path restrictions
const isAbsolute = path.isAbsolute(normalizedPath);
if (isAbsolute && !options.allowAbsolute) {
errors.push('Absolute paths are not allowed');
}
if (!isAbsolute && options.allowRelative === false) {
errors.push('Relative paths are not allowed');
}
// Check base path restriction
if (options.basePath && isAbsolute) {
const resolvedBase = path.resolve(options.basePath);
const resolvedPath = path.resolve(normalizedPath);
if (!resolvedPath.startsWith(resolvedBase)) {
errors.push('Path is outside allowed base directory');
}
}
// Check file extension
const ext = path.extname(normalizedPath).toLowerCase();
if (ext) {
if (options.blockedExtensions?.includes(ext)) {
errors.push(`File extension '${ext}' is not allowed`);
}
if (options.allowedExtensions && !options.allowedExtensions.includes(ext)) {
errors.push(`File extension '${ext}' is not in allowed list`);
}
if (this.DANGEROUS_EXTENSIONS.includes(ext)) {
errors.push(`Potentially dangerous file extension '${ext}' detected`);
}
}
// Check directory depth
if (options.maxDepth) {
const depth = normalizedPath.split(path.sep).length - 1;
if (depth > options.maxDepth) {
errors.push(`Path depth (${depth}) exceeds maximum allowed (${options.maxDepth})`);
}
}
// Check for system directories
const pathParts = normalizedPath.toLowerCase().split(path.sep);
for (const systemDir of this.SYSTEM_DIRECTORIES) {
if (pathParts.includes(systemDir)) {
errors.push(`Access to system directory '${systemDir}' is not allowed`);
}
}
return {
isValid: errors.length === 0,
errors,
...(errors.length === 0 && { sanitizedPath: normalizedPath })
};
}
/**
* Sanitizes a path by removing dangerous elements
*/
static sanitizePath(inputPath, options = {}) {
if (!inputPath || typeof inputPath !== 'string') {
return '';
}
let sanitized = inputPath;
// Remove dangerous patterns
for (const pattern of this.DANGEROUS_PATTERNS) {
sanitized = sanitized.replace(pattern, '');
}
// Remove control characters and null bytes
sanitized = sanitized.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
// Remove multiple consecutive slashes/backslashes
sanitized = sanitized.replace(/[\/\\]+/g, path.sep);
// Remove leading/trailing whitespace and path separators
sanitized = sanitized.trim().replace(/^[\/\\]+|[\/\\]+$/g, '');
// Normalize the path
try {
sanitized = path.normalize(sanitized);
}
catch {
// If normalization fails, return empty string
return '';
}
// Ensure it doesn't start with .. after sanitization
if (sanitized.startsWith('..')) {
sanitized = sanitized.replace(/^\.\.+[\/\\]?/, '');
}
// Apply length limit
if (sanitized.length > 255) {
sanitized = sanitized.substring(0, 255);
}
return sanitized;
}
/**
* Creates a safe filename from user input
*/
static createSafeFilename(filename, options = {}) {
if (!filename || typeof filename !== 'string') {
return 'untitled';
}
const maxLength = options.maxLength || 100;
let safe = filename;
// Remove path separators and dangerous characters
safe = safe.replace(/[\/\\:*?"<>|]/g, '_');
// Remove control characters
safe = safe.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
// Remove leading/trailing dots and spaces
safe = safe.replace(/^[\.\s]+|[\.\s]+$/g, '');
// Ensure it's not empty after sanitization
if (!safe) {
safe = 'untitled';
}
// Handle extension
const ext = path.extname(safe).toLowerCase();
const basename = path.basename(safe, ext);
let finalExt = ext;
if (options.allowedExtensions && ext && !options.allowedExtensions.includes(ext)) {
finalExt = options.defaultExtension || '.txt';
}
// Truncate if too long
const maxBasenameLength = maxLength - finalExt.length;
const truncatedBasename = basename.length > maxBasenameLength
? basename.substring(0, maxBasenameLength)
: basename;
return truncatedBasename + finalExt;
}
/**
* Validates multiple paths at once
*/
static validatePaths(paths, options = {}) {
const results = paths.map(inputPath => ({
path: inputPath,
...this.validatePath(inputPath, options)
}));
return {
isValid: results.every(result => result.isValid),
results
};
}
}
//# sourceMappingURL=path-traversal-prevention.js.map