-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Expand file tree
/
Copy pathpath-validation.ts
More file actions
129 lines (112 loc) · 4.35 KB
/
path-validation.ts
File metadata and controls
129 lines (112 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import path from 'path';
/**
* Normalizes a path without corrupting UNC paths.
* On Windows, path.resolve() converts UNC paths like \\server\share to C:\server\share,
* which breaks UNC path handling. This function preserves UNC paths.
*
* @param p - The path to normalize
* @returns Normalized path
*/
function normalizePathSafe(p: string): string {
// Check if it's a UNC path (starts with \\ or //) BEFORE normalizing
// We must check this first because path.normalize on macOS corrupts UNC paths
if (p.startsWith('\\\\') || p.startsWith('//')) {
// UNC paths: normalize slashes to backslashes and remove redundant slashes
// but preserve the leading \\
let normalized = p.replace(/\//g, '\\');
// Ensure exactly two leading backslashes (not more, not less)
normalized = normalized.replace(/^\\+/, '\\\\');
// Normalize any double backslashes in the rest of the path to single
// but be careful not to break the leading \\
const rest = normalized.substring(2).replace(/\\+/g, '\\');
return '\\\\' + rest;
}
// For non-UNC paths, use path.normalize then path.resolve
const normalized = path.normalize(p);
return path.resolve(normalized);
}
/**
* Checks if an absolute path is within any of the allowed directories.
*
* @param absolutePath - The absolute path to check (will be normalized)
* @param allowedDirectories - Array of absolute allowed directory paths (will be normalized)
* @returns true if the path is within an allowed directory, false otherwise
* @throws Error if given relative paths after normalization
*/
export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean {
// Type validation
if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) {
return false;
}
// Reject empty inputs
if (!absolutePath || allowedDirectories.length === 0) {
return false;
}
// Reject null bytes (forbidden in paths)
if (absolutePath.includes('\x00')) {
return false;
}
// Normalize the input path
let normalizedPath: string;
try {
normalizedPath = normalizePathSafe(absolutePath);
} catch {
return false;
}
// Verify it's absolute after normalization
// UNC paths are always absolute, as are resolved paths
const isUncPath = normalizedPath.startsWith('\\\\');
if (!isUncPath && !path.isAbsolute(normalizedPath)) {
throw new Error('Path must be absolute after normalization');
}
// Check against each allowed directory
return allowedDirectories.some(dir => {
if (typeof dir !== 'string' || !dir) {
return false;
}
// Reject null bytes in allowed dirs
if (dir.includes('\x00')) {
return false;
}
// Normalize the allowed directory
let normalizedDir: string;
try {
normalizedDir = normalizePathSafe(dir);
} catch {
return false;
}
// Verify allowed directory is absolute after normalization
const isUncDir = normalizedDir.startsWith('\\\\');
if (!isUncDir && !path.isAbsolute(normalizedDir)) {
throw new Error('Allowed directories must be absolute paths after normalization');
}
// Check if normalizedPath is within normalizedDir
// Path is inside if it's the same or a subdirectory
if (normalizedPath === normalizedDir) {
return true;
}
// Special case for root directory to avoid double slash
// On Windows, we need to check if both paths are on the same drive
if (normalizedDir === path.sep) {
return normalizedPath.startsWith(path.sep);
}
// On Windows, also check for drive root (e.g., "C:\")
if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) {
// Ensure both paths are on the same drive
const dirDrive = normalizedDir.charAt(0).toLowerCase();
const pathDrive = normalizedPath.charAt(0).toLowerCase();
return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\'));
}
// Special handling for UNC paths
// Both paths must be UNC paths for a valid match
if (isUncPath && isUncDir) {
// For UNC paths, use backslash as separator
return normalizedPath.startsWith(normalizedDir + '\\');
}
// If one is UNC and the other isn't, they can't match
if (isUncPath !== isUncDir) {
return false;
}
return normalizedPath.startsWith(normalizedDir + path.sep);
});
}