Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/bump-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
default_bump: patch
custom_tag: 1.1.0
custom_tag: 1.2.0
- name: Create a GitHub release
uses: ncipollo/release-action@v1
with:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ It means that composer will look at `master` branch of repository configured und

## Changelog

### 2026-08-26

- Security: backported the view template path containment check from CakePHP 4.5.11 (CVE-2026-48820 / GHSA-wpvj-hjcr-h3p2). Element / view / layout names resolving outside the configured view template paths now throw `InvalidArgumentException`. BC note: `elementExists()` now throws for such names instead of returning `false`.

### 2025-02-04

- Fixes for PHP 8.4: `session_set_save_handler` accepts object, removed `E_STRICT` reference.
Expand Down
82 changes: 82 additions & 0 deletions lib/Cake/Test/Case/View/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,16 @@ public function getLayoutFileName($name = null) {
return $this->_getLayoutFileName($name);
}

/**
* getElementFileName method
*
* @param string $name The name of the element to find.
* @return mixed Either a string to the element filename or false when one can't be found.
*/
public function getElementFileName($name) {
return $this->_getElementFileName($name);
}

/**
* paths method
*
Expand Down Expand Up @@ -948,6 +958,78 @@ public function testElementCtpFallback() {
$this->assertEquals($expected, $result);
}

/**
* Test that elements outside of the view paths cannot be rendered.
*
* @return void
*/
public function testElementPathEscape() {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('it is not within any view template path.');
$this->View->element('../../../../Console/Templates/default/views/index');
}

/**
* Test that relative paths resolving inside the Elements directory still work.
*
* @return void
*/
public function testElementPathRelativeInsideElements() {
$result = $this->View->element('nocache/../test_element');
$this->assertSame('this is the test element', $result);
}

/**
* Test that relative paths leaving Elements but staying inside a view path work.
*
* @return void
*/
public function testElementPathRelativeInsideViewPath() {
$View = new TestView($this->PostsController);
$file = $View->getElementFileName('../Posts/index');
$expected = CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS . 'Posts' . DS . 'index.ctp';
$this->assertSame(realpath($expected), $file);

$result = $View->element('../Posts/index');
$this->assertSame('posts index', $result);
}

/**
* Test that layouts outside of the view paths cannot be resolved.
*
* @return void
*/
public function testLayoutPathEscape() {
$View = new TestView($this->PostsController);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('it is not within any view template path.');
$View->getLayoutFileName('../../../../Console/Templates/default/views/index');
}

/**
* Test that views outside of the view paths cannot be resolved.
*
* @return void
*/
public function testViewPathEscape() {
$View = new TestView($this->PostsController);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('it is not within any view template path.');
$View->getViewFileName('../../../../Console/Templates/default/views/index');
}

/**
* Test that the leading-slash view name branch cannot escape the view paths.
*
* @return void
*/
public function testViewPathEscapeLeadingSlash() {
$View = new TestView($this->PostsController);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('it is not within any view template path.');
$View->getViewFileName('/../../../Console/Templates/default/views/index');
}

/**
* Test loadHelpers method
*
Expand Down
3 changes: 2 additions & 1 deletion lib/Cake/View/ScaffoldView.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ScaffoldView extends View {
* @param string $name name of the view file to get.
* @return string action
* @throws MissingViewException
* @throws InvalidArgumentException when the resolved file is outside the view paths.
*/
protected function _getViewFileName($name = null) {
if ($name === null) {
Expand Down Expand Up @@ -75,7 +76,7 @@ protected function _getViewFileName($name = null) {
foreach ($paths as $path) {
foreach ($names as $name) {
if (file_exists($path . $name . $ext)) {
return $path . $name . $ext;
return $this->_checkFilePath($path . $name . $ext, $this->plugin);
}
}
}
Expand Down
46 changes: 43 additions & 3 deletions lib/Cake/View/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ public function loadHelper($helperName, $settings = array()) {
* @param string $name Controller action to find template filename for
* @return string Template filename
* @throws MissingViewException when a view file could not be found.
* @throws InvalidArgumentException when the resolved file is outside the view paths.
*/
protected function _getViewFileName($name = null) {
$subDir = null;
Expand Down Expand Up @@ -1024,7 +1025,7 @@ protected function _getViewFileName($name = null) {
foreach ($exts as $ext) {
foreach ($paths as $path) {
if (file_exists($path . $name . $ext)) {
return $path . $name . $ext;
return $this->_checkFilePath($path . $name . $ext, $plugin);
}
}
}
Expand Down Expand Up @@ -1059,6 +1060,7 @@ public function pluginSplit($name, $fallback = true) {
* @param string $name The name of the layout to find.
* @return string Filename for layout file (.ctp).
* @throws MissingLayoutException when a layout cannot be located
* @throws InvalidArgumentException when the resolved file is outside the view paths.
*/
protected function _getLayoutFileName($name = null) {
if ($name === null) {
Expand All @@ -1077,7 +1079,7 @@ protected function _getLayoutFileName($name = null) {
foreach ($exts as $ext) {
foreach ($paths as $path) {
if (file_exists($path . $file . $ext)) {
return $path . $file . $ext;
return $this->_checkFilePath($path . $file . $ext, $plugin);
}
}
}
Expand All @@ -1102,6 +1104,7 @@ protected function _getExtensions() {
*
* @param string $name The name of the element to find.
* @return mixed Either a string to the element filename or false when one can't be found.
* @throws InvalidArgumentException when the resolved file is outside the view paths.
*/
protected function _getElementFileName($name) {
list($plugin, $name) = $this->pluginSplit($name);
Expand All @@ -1111,13 +1114,50 @@ protected function _getElementFileName($name) {
foreach ($exts as $ext) {
foreach ($paths as $path) {
if (file_exists($path . 'Elements' . DS . $name . $ext)) {
return $path . 'Elements' . DS . $name . $ext;
return $this->_checkFilePath($path . 'Elements' . DS . $name . $ext, $plugin);
}
}
}
return false;
}

/**
* Check that a resolved template path is contained within one of the view paths.
*
* Paths that contain no `..` segment cannot escape the view path they were
* built from, so they are returned as is. Any other path is resolved with
* realpath() and must be contained in one of View::_paths(). The canonical
* path is returned after validation.
*
* @param string $file The resolved path to the template file.
* @param string $plugin The plugin the file was resolved for, if any.
* @return string The unmodified or canonical file path.
* @throws InvalidArgumentException When the file is not within a view path.
*/
protected function _checkFilePath($file, $plugin = null) {
if (strpos($file, '..') === false) {
return $file;
}
$absolute = realpath($file);
if ($absolute === false) {
return $file;
}
foreach ($this->_paths($plugin) as $path) {
if (strpos($absolute, rtrim($path, DS) . DS) === 0) {
return $absolute;
}
$root = realpath($path);
if ($root !== false && strpos($absolute, rtrim($root, DS) . DS) === 0) {
return $absolute;
}
}
throw new InvalidArgumentException(__d(
'cake_dev',
'Cannot use "%s" as a template, it is not within any view template path.',
$file
));
}

/**
* Return all possible paths to find view files in order
*
Expand Down
Loading