Skip to content

Commit 18d77b1

Browse files
committed
Add canonicalizePath method to resolve relative segments in paths; update related path checks for consistency
1 parent 0edc2bc commit 18d77b1

1 file changed

Lines changed: 59 additions & 15 deletions

File tree

src/Internal/PathMatcher.php

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,52 @@ public static function normalizePath(string|false|null $path): string
6464
return str_replace('\\', '/', $path);
6565
}
6666

67+
/**
68+
* Resolves dot-dot (..) relative segments in a path without hitting the disk.
69+
*/
70+
public static function canonicalizePath(string $path): string
71+
{
72+
$path = str_replace('\\', '/', $path);
73+
if (! str_contains($path, '..')) {
74+
return $path;
75+
}
76+
77+
$parts = explode('/', $path);
78+
$absolutes = [];
79+
80+
foreach ($parts as $part) {
81+
if ($part === '.' || $part === '') {
82+
if ($part === '' && empty($absolutes)) {
83+
$absolutes[] = '';
84+
}
85+
continue;
86+
}
87+
88+
if ($part === '..') {
89+
if (! empty($absolutes) && end($absolutes) !== '..') {
90+
array_pop($absolutes);
91+
} else {
92+
$absolutes[] = '..';
93+
}
94+
} else {
95+
$absolutes[] = $part;
96+
}
97+
}
98+
99+
return implode('/', $absolutes);
100+
}
101+
67102
/**
68103
* Determines whether a given path is located within a vendor directory.
69104
*/
70105
public static function isVendorPath(string $normalizedPath, string $rawPath = ''): bool
71106
{
72-
$normalizedRaw = self::normalizePath($rawPath);
107+
$canon = self::canonicalizePath($normalizedPath);
108+
$canonRaw = $rawPath !== '' ? self::canonicalizePath($rawPath) : '';
73109

74-
return str_starts_with($normalizedPath, 'vendor/')
75-
|| str_contains($normalizedPath, '/vendor/')
76-
|| ($normalizedRaw !== '' && (str_starts_with($normalizedRaw, 'vendor/') || str_contains($normalizedRaw, '/vendor/')));
110+
return str_starts_with($canon, 'vendor/')
111+
|| str_contains($canon, '/vendor/')
112+
|| ($canonRaw !== '' && (str_starts_with($canonRaw, 'vendor/') || str_contains($canonRaw, '/vendor/')));
77113
}
78114

79115
/**
@@ -160,10 +196,12 @@ public static function hasIncludeMatchingPrefix(string $prefix, array $includes)
160196
*/
161197
public static function isDynamicWritablePath(string $normalizedPath): bool
162198
{
163-
return str_contains($normalizedPath, '/var/cache/') || str_starts_with($normalizedPath, 'var/cache/')
164-
|| str_contains($normalizedPath, '/var/log/') || str_starts_with($normalizedPath, 'var/log/')
165-
|| str_contains($normalizedPath, '/storage/') || str_starts_with($normalizedPath, 'storage/')
166-
|| str_contains($normalizedPath, '/cache/') || str_starts_with($normalizedPath, 'cache/');
199+
$canon = self::canonicalizePath($normalizedPath);
200+
201+
return str_contains($canon, '/var/cache/') || str_starts_with($canon, 'var/cache/')
202+
|| str_contains($canon, '/var/log/') || str_starts_with($canon, 'var/log/')
203+
|| str_contains($canon, '/storage/') || str_starts_with($canon, 'storage/')
204+
|| str_contains($canon, '/cache/') || str_starts_with($canon, 'cache/');
167205
}
168206

169207
/**
@@ -172,31 +210,35 @@ public static function isDynamicWritablePath(string $normalizedPath): bool
172210
*/
173211
public static function mayPathBeIncluded(string $normalizedPath): bool
174212
{
175-
if (str_contains($normalizedPath, '/node_modules/') || str_starts_with($normalizedPath, 'node_modules/')) {
213+
$canon = self::canonicalizePath($normalizedPath);
214+
215+
if (str_contains($canon, '/node_modules/') || str_starts_with($canon, 'node_modules/')) {
176216
return false;
177217
}
178218

179-
if (self::isCachePath($normalizedPath)) {
219+
if (self::isCachePath($canon)) {
180220
return false;
181221
}
182222

183223
$config = Config::get();
184224
/** @var array<int, string> $includes */
185225
$includes = \is_array($config['include'] ?? null) ? $config['include'] : ['**'];
186226

187-
if (str_contains($normalizedPath, '/vendor/') || str_starts_with($normalizedPath, 'vendor/')) {
227+
if (str_contains($canon, '/vendor/') || str_starts_with($canon, 'vendor/')) {
188228
if (! self::hasIncludeMatchingPrefix('vendor/', $includes)) {
189229
return false;
190230
}
191231
}
192232

193-
if (str_contains($normalizedPath, '/var/') || str_starts_with($normalizedPath, 'var/')) {
233+
// 4. var/ check: only inspect if user explicitly whitelisted var
234+
if (str_contains($canon, '/var/') || str_starts_with($canon, 'var/')) {
194235
if (! self::hasIncludeMatchingPrefix('var/', $includes)) {
195236
return false;
196237
}
197238
}
198239

199-
if (str_contains($normalizedPath, '/storage/') || str_starts_with($normalizedPath, 'storage/')) {
240+
// 5. storage/ check: only inspect if user explicitly whitelisted storage
241+
if (str_contains($canon, '/storage/') || str_starts_with($canon, 'storage/')) {
200242
if (! self::hasIncludeMatchingPrefix('storage/', $includes)) {
201243
return false;
202244
}
@@ -241,7 +283,8 @@ public static function isPathIncluded(
241283
?string $baseDir = null
242284
): bool {
243285
$baseDir = $baseDir !== null ? self::normalizePath($baseDir) : Config::getProjectRoot();
244-
$normalizedRaw = self::normalizePath($rawPath);
286+
$normalizedPath = self::canonicalizePath($normalizedPath);
287+
$normalizedRaw = $rawPath !== '' ? self::canonicalizePath(self::normalizePath($rawPath)) : '';
245288

246289
$includes = self::getCompiledPatterns($includeGlobs, $baseDir, 'include');
247290
$excludes = self::getCompiledPatterns($excludeGlobs, $baseDir, 'exclude');
@@ -250,7 +293,8 @@ public static function isPathIncluded(
250293
if ($isVendor) {
251294
$hasExplicitVendorWhitelist = false;
252295
foreach ($includes as $compiled) {
253-
if (str_starts_with($compiled['pattern'], 'vendor/') &&
296+
if (
297+
str_starts_with($compiled['pattern'], 'vendor/') &&
254298
(preg_match($compiled['regex'], $normalizedPath) === 1 || ($normalizedRaw !== '' && preg_match($compiled['regex'], $normalizedRaw) === 1))
255299
) {
256300
$hasExplicitVendorWhitelist = true;

0 commit comments

Comments
 (0)