Skip to content

Commit f16c3e5

Browse files
committed
bug #2707 - do not use realpath() per default to avoid conflicts with open_basedir, strong path normalisation
1 parent 3b71efb commit f16c3e5

2 files changed

Lines changed: 64 additions & 9 deletions

File tree

lib/Twig/Loader/Filesystem.php

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,25 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
2323
protected $cache = array();
2424
protected $errorCache = array();
2525

26+
private $useRealpath = false;
2627
private $rootPath;
2728

2829
/**
2930
* @param string|array $paths A path or an array of paths where to look for templates
3031
* @param string|null $rootPath The root path common to all relative paths (null for getcwd())
3132
*/
32-
public function __construct($paths = array(), $rootPath = null)
33+
public function __construct($paths = array(), $rootPath = null, $useRealpath = false)
3334
{
34-
$this->rootPath = (null === $rootPath ? getcwd() : $rootPath).DIRECTORY_SEPARATOR;
35-
if (false !== $realPath = realpath($rootPath)) {
36-
$this->rootPath = $realPath.DIRECTORY_SEPARATOR;
35+
$this->useRealpath = $useRealpath;
36+
37+
$this->rootPath = (null === $rootPath ? getcwd() : $rootPath);
38+
if ($this->rootPath) {
39+
// realpath() usage for backward compatibility only
40+
if ($useRealpath && false !== ($realPath = realpath($this->rootPath))) {
41+
$this->rootPath = $realPath.DIRECTORY_SEPARATOR;
42+
} else {
43+
$this->rootPath = $this->normalizePath($this->rootPath).DIRECTORY_SEPARATOR;
44+
}
3745
}
3846

3947
if ($paths) {
@@ -214,12 +222,12 @@ protected function findTemplate($name)
214222
$path = $this->rootPath.'/'.$path;
215223
}
216224

217-
if (is_file($path.'/'.$shortname)) {
218-
if (false !== $realpath = realpath($path.'/'.$shortname)) {
219-
return $this->cache[$name] = $realpath;
225+
$filename = $path.'/'.$shortname;
226+
if (is_file($filename)) {
227+
if ($this->useRealpath && false !== ($realPath = realpath($filename))) {
228+
$filename = $realPath;
220229
}
221-
222-
return $this->cache[$name] = $path.'/'.$shortname;
230+
return $this->cache[$name] = $this->normalizePath($filename);
223231
}
224232
}
225233

@@ -275,6 +283,39 @@ protected function validateName($name)
275283
}
276284
}
277285

286+
/**
287+
* Normalize a path by removing redundant '..' and thus preventing the need
288+
* of using the realpath() function that may come with some side effects
289+
*
290+
* @param string $string
291+
* @param bool $removeTrailingSlash
292+
* @return string
293+
*/
294+
private function normalizePath($string, $removeTrailingSlash = false)
295+
{
296+
if (DIRECTORY_SEPARATOR !== '/') { // Handle windows gracefully
297+
$string = str_replace(DIRECTORY_SEPARATOR, '/', $string);
298+
}
299+
300+
// Preserve scheme
301+
// This leaves room for performance improvement
302+
$scheme = null;
303+
if (strpos($string, '://')) {
304+
list($scheme, $string) = explode('://', $string, 2);
305+
}
306+
307+
$count = 0; // This leaves room for performance improvement too
308+
do {
309+
$string = preg_replace('@[^/]+/+..(/+|$)@', '$2', preg_replace('@//+@', '/', $string), -1, $count);
310+
} while ($count);
311+
312+
if ($removeTrailingSlash) {
313+
$string = rtrim($string, '/');
314+
}
315+
316+
return $scheme ? ($scheme.'://'.$string) : $string;
317+
}
318+
278319
private function isAbsolutePath($file)
279320
{
280321
return strspn($file, '/\\', 0, 1)

test/Twig/Tests/Loader/FilesystemTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,19 @@ public function testLoadTemplateFromPhar()
222222
// $f->addFromString('hello.twig', 'hello from phar');
223223
$loader->addPath('phar://'.dirname(__FILE__).'/Fixtures/phar/phar-sample.phar');
224224
$this->assertSame('hello from phar', $loader->getSourceContext('hello.twig')->getCode());
225+
// Also attempt filename normalization within a schemed path
226+
$this->assertSame('hello from phar', $loader->getSourceContext('hello.twig')->getCode());
227+
// And within the filename itself
228+
}
229+
230+
/**
231+
* @requires PHP 5.3
232+
*/
233+
public function testLoadTemplateFromPharNormalization()
234+
{
235+
$loader = new Twig_Loader_Filesystem(array());
236+
$loader->addPath('phar://'.dirname(__FILE__).'/Fixtures/phar/non-existing-segment/../phar-sample.phar');
237+
$this->assertSame('hello from phar', $loader->getSourceContext('hello.twig')->getCode());
238+
$this->assertSame('hello from phar', $loader->getSourceContext('another-non-existing-segment/../hello.twig')->getCode());
225239
}
226240
}

0 commit comments

Comments
 (0)