From 8093a18fb33206de38055e664ff28be2f3409311 Mon Sep 17 00:00:00 2001 From: Ambrose Casanova <279373485+ambrose5773@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:57:48 -0600 Subject: [PATCH] fix: reject Windows view paths that leave the views directory Normalize both separators before the containment check, and build the regression next to the views directory so it stays on the same drive. --- flight/template/View.php | 14 ++++++++------ tests/ViewTest.php | 28 +++++++++------------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/flight/template/View.php b/flight/template/View.php index 24a5724..2e81ae3 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -222,17 +222,19 @@ class View */ private function relativeStaysInside(string $file): bool { - $segments = \preg_split('#[\\/]+#', $file, -1, \PREG_SPLIT_NO_EMPTY); - if ($segments === false) { - return false; - } - + $segments = \explode('/', \str_replace('\\', '/', $file)); $depth = 0; + foreach ($segments as $segment) { - if ($segment === '.') { + if ($segment === '' || $segment === '.') { continue; } + // A drive letter or colon is an absolute jump, not a view name. + if (\strpos($segment, ':') !== false) { + return false; + } + if ($segment === '..') { if ($depth === 0) { return false; diff --git a/tests/ViewTest.php b/tests/ViewTest.php index 60162fb..ac360d2 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -138,14 +138,11 @@ class ViewTest extends TestCase public function testRejectsTemplateThatLeavesViewsDirectory(): void { - $outside = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-outside-' . uniqid(); + $outside = dirname($this->view->path) . DIRECTORY_SEPARATOR . 'flight-view-outside-' . uniqid(); mkdir($outside); $note = $outside . DIRECTORY_SEPARATOR . 'note.php'; file_put_contents($note, 'view->path); - $relative = $this->relativePathFrom($views, $note); - $relative = preg_replace('/\.php$/', '', $relative); + $relative = '..' . DIRECTORY_SEPARATOR . basename($outside) . DIRECTORY_SEPARATOR . 'note'; try { $this->expectException(Exception::class); @@ -157,6 +154,13 @@ class ViewTest extends TestCase } } + public function testRejectsEmbeddedDriveLetter(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template path is not allowed.'); + $this->view->getTemplate('layouts' . DIRECTORY_SEPARATOR . 'C:' . DIRECTORY_SEPARATOR . 'outside'); + } + public function testRejectsTemplateThatResolvesOutsideViewsDirectory(): void { $root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-root-' . uniqid(); @@ -194,20 +198,6 @@ class ViewTest extends TestCase $view->render('hello'); } - private function relativePathFrom(string $fromDir, string $toFile): string - { - $from = explode(DIRECTORY_SEPARATOR, rtrim($fromDir, DIRECTORY_SEPARATOR)); - $to = explode(DIRECTORY_SEPARATOR, $toFile); - $file = array_pop($to); - - while ($from !== [] && $to !== [] && $from[0] === $to[0]) { - array_shift($from); - array_shift($to); - } - - $up = array_fill(0, count($from), '..'); - return implode(DIRECTORY_SEPARATOR, array_merge($up, $to, [$file])); - } private function removeDir(string $dir): void {