diff --git a/lib/Cake/Controller/Component/PaginatorComponent.php b/lib/Cake/Controller/Component/PaginatorComponent.php index dcefbe703..e5d5fb425 100644 --- a/lib/Cake/Controller/Component/PaginatorComponent.php +++ b/lib/Cake/Controller/Component/PaginatorComponent.php @@ -121,7 +121,8 @@ class PaginatorComponent extends Component { * @param Model|string $object Model to paginate (e.g: model instance, or 'Model', or 'Model.InnerModel') * @param string|array $scope Additional find conditions to use while paginating * @param array $whitelist List of allowed fields for ordering. This allows you to prevent ordering - * on non-indexed, or undesirable columns. + * on non-indexed, or undesirable columns. See PaginatorComponent::validateSort() for additional details + * on how the whitelisting and sort field validation works. * @return array Model query results * @throws MissingModelException * @throws NotFoundException @@ -351,6 +352,9 @@ class PaginatorComponent extends Component { * You can use the whitelist parameter to control which columns/fields are available for sorting. * This helps prevent users from ordering large result sets on un-indexed values. * + * Any columns listed in the sort whitelist will be implicitly trusted. You can use this to sort + * on synthetic columns, or columns added in custom find operations that may not exist in the schema. + * * @param Model $object The model being paginated. * @param array $options The pagination options being used for this request. * @param array $whitelist The list of columns that can be used for sorting. If empty all keys are allowed. @@ -370,10 +374,11 @@ class PaginatorComponent extends Component { if (!empty($whitelist) && isset($options['order']) && is_array($options['order'])) { $field = key($options['order']); - if (!in_array($field, $whitelist)) { + $inWhitelist = in_array($field, $whitelist, true); + if (!$inWhitelist) { $options['order'] = null; - return $options; } + return $options; } if (!empty($options['order']) && is_array($options['order'])) { diff --git a/lib/Cake/Model/Datasource/Database/Mysql.php b/lib/Cake/Model/Datasource/Database/Mysql.php index e456ea744..d7e143090 100644 --- a/lib/Cake/Model/Datasource/Database/Mysql.php +++ b/lib/Cake/Model/Datasource/Database/Mysql.php @@ -136,20 +136,24 @@ class Mysql extends DboSource { public function connect() { $config = $this->config; $this->connected = false; + + $flags = array( + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + + if (!empty($config['encoding'])) { + $flags[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $config['encoding']; + } + + if (empty($config['unix_socket'])) { + $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; + } else { + $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}"; + } + try { - $flags = array( - PDO::ATTR_PERSISTENT => $config['persistent'], - PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - if (!empty($config['encoding'])) { - $flags[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $config['encoding']; - } - if (empty($config['unix_socket'])) { - $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; - } else { - $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}"; - } $this->_connection = new PDO( $dsn, $config['login'], diff --git a/lib/Cake/Model/Datasource/Database/Postgres.php b/lib/Cake/Model/Datasource/Database/Postgres.php index 826e1b701..e76bcb772 100644 --- a/lib/Cake/Model/Datasource/Database/Postgres.php +++ b/lib/Cake/Model/Datasource/Database/Postgres.php @@ -110,11 +110,13 @@ class Postgres extends DboSource { public function connect() { $config = $this->config; $this->connected = false; + + $flags = array( + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + try { - $flags = array( - PDO::ATTR_PERSISTENT => $config['persistent'], - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); $this->_connection = new PDO( "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}", $config['login'], diff --git a/lib/Cake/Model/Datasource/Database/Sqlserver.php b/lib/Cake/Model/Datasource/Database/Sqlserver.php index 6845fad00..455710206 100644 --- a/lib/Cake/Model/Datasource/Database/Sqlserver.php +++ b/lib/Cake/Model/Datasource/Database/Sqlserver.php @@ -117,14 +117,17 @@ class Sqlserver extends DboSource { public function connect() { $config = $this->config; $this->connected = false; + + $flags = array( + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + + if (!empty($config['encoding'])) { + $flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding']; + } + try { - $flags = array( - PDO::ATTR_PERSISTENT => $config['persistent'], - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - if (!empty($config['encoding'])) { - $flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding']; - } $this->_connection = new PDO( "sqlsrv:server={$config['host']};Database={$config['database']}", $config['login'], diff --git a/lib/Cake/Network/CakeResponse.php b/lib/Cake/Network/CakeResponse.php index 9d680959b..ab3d36542 100644 --- a/lib/Cake/Network/CakeResponse.php +++ b/lib/Cake/Network/CakeResponse.php @@ -1364,7 +1364,7 @@ class CakeResponse { break; } if ($end && $offset + $bufferSize >= $end) { - $bufferSize = $end - $offset; + $bufferSize = $end - $offset + 1; } echo fread($file->handle, $bufferSize); if (!$compress) { diff --git a/lib/Cake/Routing/Filter/AssetDispatcher.php b/lib/Cake/Routing/Filter/AssetDispatcher.php index 6b00588d7..2c8d165b0 100644 --- a/lib/Cake/Routing/Filter/AssetDispatcher.php +++ b/lib/Cake/Routing/Filter/AssetDispatcher.php @@ -42,7 +42,7 @@ class AssetDispatcher extends DispatcherFilter { * @return CakeResponse if the client is requesting a recognized asset, null otherwise */ public function beforeDispatch(CakeEvent $event) { - $url = $event->data['request']->url; + $url = urldecode($event->data['request']->url); if (strpos($url, '..') !== false || strpos($url, '.') === false) { return; } @@ -118,7 +118,7 @@ class AssetDispatcher extends DispatcherFilter { if ($parts[0] === 'theme') { $themeName = $parts[1]; unset($parts[0], $parts[1]); - $fileFragment = urldecode(implode(DS, $parts)); + $fileFragment = implode(DS, $parts); $path = App::themePath($themeName) . 'webroot' . DS; return $path . $fileFragment; } @@ -126,7 +126,7 @@ class AssetDispatcher extends DispatcherFilter { $plugin = Inflector::camelize($parts[0]); if ($plugin && CakePlugin::loaded($plugin)) { unset($parts[0]); - $fileFragment = urldecode(implode(DS, $parts)); + $fileFragment = implode(DS, $parts); $pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS; return $pluginWebroot . $fileFragment; } diff --git a/lib/Cake/Test/Case/Controller/Component/PaginatorComponentTest.php b/lib/Cake/Test/Case/Controller/Component/PaginatorComponentTest.php index 42778a494..df0cc9b21 100644 --- a/lib/Cake/Test/Case/Controller/Component/PaginatorComponentTest.php +++ b/lib/Cake/Test/Case/Controller/Component/PaginatorComponentTest.php @@ -933,6 +933,23 @@ class PaginatorComponentTest extends CakeTestCase { $this->assertNull($result['order']); } +/** + * test that fields in the whitelist are not validated + * + * @return void + */ + public function testValidateSortWhitelistTrusted() { + $model = $this->getMock('Model'); + $model->alias = 'model'; + $model->expects($this->never())->method('hasField'); + + $options = array('sort' => 'body', 'direction' => 'asc'); + $result = $this->Paginator->validateSort($model, $options, array('body')); + + $expected = array('body' => 'asc'); + $this->assertEquals($expected, $result['order']); + } + /** * test that virtual fields work. * diff --git a/lib/Cake/Test/Case/Network/CakeResponseTest.php b/lib/Cake/Test/Case/Network/CakeResponseTest.php index 952efc47d..5bc04b759 100644 --- a/lib/Cake/Test/Case/Network/CakeResponseTest.php +++ b/lib/Cake/Test/Case/Network/CakeResponseTest.php @@ -1437,7 +1437,7 @@ class CakeResponseTest extends CakeTestCase { $result = $response->send(); $output = ob_get_clean(); $this->assertEquals(206, $response->statusCode()); - $this->assertEquals("is the test asset", $output); + $this->assertEquals("is the test asset ", $output); $this->assertTrue($result !== false); } diff --git a/lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php b/lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php index 4021e5230..e8f028301 100644 --- a/lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php +++ b/lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php @@ -143,4 +143,48 @@ class AssetDispatcherTest extends CakeTestCase { $this->assertFalse($event->isStopped()); } +/** + * Test that attempts to traverse directories are prevented. + * + * @return void + */ + public function test404OnDoubleDot() { + App::build(array( + 'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS), + 'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS) + ), APP::RESET); + + $response = $this->getMock('CakeResponse', array('_sendHeader')); + $request = new CakeRequest('theme/test_theme/../../../../../../VERSION.txt'); + $event = new CakeEvent('Dispatcher.beforeRequest', $this, compact('request', 'response')); + + $response->expects($this->never())->method('send'); + + $filter = new AssetDispatcher(); + $this->assertNull($filter->beforeDispatch($event)); + $this->assertFalse($event->isStopped()); + } + +/** + * Test that attempts to traverse directories with urlencoded paths fail. + * + * @return void + */ + public function test404OnDoubleDotEncoded() { + App::build(array( + 'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS), + 'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS) + ), APP::RESET); + + $response = $this->getMock('CakeResponse', array('_sendHeader', 'send')); + $request = new CakeRequest('theme/test_theme/%2e./%2e./%2e./%2e./%2e./%2e./VERSION.txt'); + $event = new CakeEvent('Dispatcher.beforeRequest', $this, compact('request', 'response')); + + $response->expects($this->never())->method('send'); + + $filter = new AssetDispatcher(); + $this->assertNull($filter->beforeDispatch($event)); + $this->assertFalse($event->isStopped()); + } + } diff --git a/lib/Cake/Test/Case/View/HelperTest.php b/lib/Cake/Test/Case/View/HelperTest.php index ce24110d1..a35dcac3d 100644 --- a/lib/Cake/Test/Case/View/HelperTest.php +++ b/lib/Cake/Test/Case/View/HelperTest.php @@ -677,8 +677,8 @@ class HelperTest extends CakeTestCase { 'here' => '/cake_dev/index.php/tasks', )); $result = $this->Helper->assetUrl('img/cake.icon.png', array('fullBase' => true)); - - $this->assertEquals($result, FULL_BASE_URL . '/cake_dev/app/webroot/img/cake.icon.png'); + $expected = FULL_BASE_URL . '/cake_dev/app/webroot/img/cake.icon.png'; + $this->assertEquals($expected, $result); } /**