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.
pull/729/head
Ambrose Casanova 4 days ago
parent 63e5bcd613
commit 8093a18fb3

@ -222,17 +222,19 @@ class View
*/ */
private function relativeStaysInside(string $file): bool private function relativeStaysInside(string $file): bool
{ {
$segments = \preg_split('#[\\/]+#', $file, -1, \PREG_SPLIT_NO_EMPTY); $segments = \explode('/', \str_replace('\\', '/', $file));
if ($segments === false) {
return false;
}
$depth = 0; $depth = 0;
foreach ($segments as $segment) { foreach ($segments as $segment) {
if ($segment === '.') { if ($segment === '' || $segment === '.') {
continue; continue;
} }
// A drive letter or colon is an absolute jump, not a view name.
if (\strpos($segment, ':') !== false) {
return false;
}
if ($segment === '..') { if ($segment === '..') {
if ($depth === 0) { if ($depth === 0) {
return false; return false;

@ -138,14 +138,11 @@ class ViewTest extends TestCase
public function testRejectsTemplateThatLeavesViewsDirectory(): void 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); mkdir($outside);
$note = $outside . DIRECTORY_SEPARATOR . 'note.php'; $note = $outside . DIRECTORY_SEPARATOR . 'note.php';
file_put_contents($note, '<?php echo "blocked";'); file_put_contents($note, '<?php echo "blocked";');
$relative = '..' . DIRECTORY_SEPARATOR . basename($outside) . DIRECTORY_SEPARATOR . 'note';
$views = realpath($this->view->path);
$relative = $this->relativePathFrom($views, $note);
$relative = preg_replace('/\.php$/', '', $relative);
try { try {
$this->expectException(Exception::class); $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 public function testRejectsTemplateThatResolvesOutsideViewsDirectory(): void
{ {
$root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-root-' . uniqid(); $root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-root-' . uniqid();
@ -194,20 +198,6 @@ class ViewTest extends TestCase
$view->render('hello'); $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 private function removeDir(string $dir): void
{ {

Loading…
Cancel
Save