| 301 | * ``` |
| 302 | */ |
| 303 | export function sanitizePath(path: string): string { |
| 304 | let sanitized = path.replace(/\0/g, '') |
| 305 | sanitized = sanitized.trim() |
| 306 | |
| 307 | if (sanitized.includes('%00')) { |
| 308 | logger.warn('Path contains URL-encoded null bytes', { |
| 309 | path: path.substring(0, 100), |
| 310 | }) |
| 311 | throw new Error('Path contains invalid characters') |
| 312 | } |
| 313 | |
| 314 | const pathTraversalPatterns = [ |
| 315 | '../', // Standard Unix path traversal |
| 316 | '..\\', // Windows path traversal |
| 317 | '/../', // Mid-path traversal |
| 318 | '\\..\\', // Windows mid-path traversal |
| 319 | '%2e%2e%2f', // Fully encoded ../ |
| 320 | '%2e%2e/', // Partially encoded ../ |
| 321 | '%2e%2e%5c', // Fully encoded ..\ |
| 322 | '%2e%2e\\', // Partially encoded ..\ |
| 323 | '..%2f', // .. with encoded / |
| 324 | '..%5c', // .. with encoded \ |
| 325 | '%252e%252e', // Double URL encoded .. |
| 326 | '..%252f', // .. with double encoded / |
| 327 | '..%255c', // .. with double encoded \ |
| 328 | ] |
| 329 | |
| 330 | const lowerPath = sanitized.toLowerCase() |
| 331 | for (const pattern of pathTraversalPatterns) { |
| 332 | if (lowerPath.includes(pattern.toLowerCase())) { |
| 333 | logger.warn('Path traversal attempt detected', { |
| 334 | pattern, |
| 335 | path: path.substring(0, 100), |
| 336 | }) |
| 337 | throw new Error('Path contains invalid path traversal sequences') |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | const segments = sanitized.split(/[/\\]/) |
| 342 | for (const segment of segments) { |
| 343 | if (segment === '..') { |
| 344 | logger.warn('Path traversal attempt detected (.. as path segment)', { |
| 345 | path: path.substring(0, 100), |
| 346 | }) |
| 347 | throw new Error('Path contains invalid path traversal sequences') |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | return sanitized |
| 352 | } |
| 353 | |
| 354 | /** |
| 355 | * Escape a string for safe use in single-quoted shell arguments |