fix: reject dangling symlinks in validate_relative_path_within (#8389)

* fix: reject dangling symlinks in validate_relative_path_within

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* docs: add changelog entry for dangling symlink fix

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-10-14 22:38:41 -04:00
1 parent 37a44d793e
commit 0f06608175
3 files changed
+20 -2

No files matched your search

+1
View File
@@ -0,0 +1 @@
Reject dangling symlinks in validate_relative_path_within
+1 -1
View File
@@ -10787,7 +10787,7 @@ function validate_relative_path_within(mixed $path, string $base_dir) : mixed {
foreach ($parts as $part) {
$walk .= '/' . $part;
if (file_exists($walk) && is_link($walk)) {
if (is_link($walk)) {
return false;
}
}
+18 -1
View File
@@ -28,10 +28,12 @@ beforeEach(function () : void {
$this->base = sys_get_temp_dir() . '/cacti_containment_' . bin2hex(random_bytes(6));
mkdir($this->base . '/nested', 0755, true);
file_put_contents($this->base . '/nested/present.txt', 'x');
$this->outside = $this->base . '_outside/pwned.txt';
});
afterEach(function () : void {
foreach (['/nested/present.txt', '/nested/link', '/link', '/nested', ''] as $leaf) {
foreach (['/nested/present.txt', '/nested/link', '/nested/dangling.php', '/link', '/dangling', '/nested', ''] as $leaf) {
$path = $this->base . $leaf;
if (is_link($path)) {
@@ -84,6 +86,21 @@ test('a symlinked segment under the base is rejected', function () : void {
expect(validate_relative_path_within('link/anything.txt', $this->base))->toBeFalse();
});
test('a dangling symlink is rejected as the final or an intermediate segment', function () : void {
if (DIRECTORY_SEPARATOR === '\\') {
$this->markTestSkipped('Symlink creation is not consistently available on Windows.');
}
if (!@symlink($this->outside, $this->base . '/nested/dangling.php') || !@symlink($this->outside, $this->base . '/dangling')) {
$this->markTestSkipped('Symlink creation is not permitted in this environment.');
}
expect(validate_relative_path_within('nested/dangling.php', $this->base))->toBeFalse()
->and(validate_relative_path_within('dangling/child.php', $this->base))->toBeFalse()
->and(validate_relative_path_within('nested/present.txt', $this->base))
->toBe(realpath($this->base) . '/nested/present.txt');
});
test('a path whose parent cannot be resolved is rejected', function () : void {
expect(validate_relative_path_within('missing_dir/child/file.txt', $this->base))->toBeFalse();
});