diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php index 937953f..487fa6e 100644 --- a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php @@ -119,17 +119,29 @@ protected function getLocalPath($uri = NULL) { if (!isset($uri)) { $uri = $this->uri; } - $path = $this->getDirectoryPath() . '/' . $this->getTarget($uri); - $realpath = realpath($path); - if (!$realpath) { - // This file does not yet exist. - $realpath = realpath(dirname($path)) . '/' . drupal_basename($path); - } - $directory = realpath($this->getDirectoryPath()); - if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) { + // Get the target path relative to the files repository. + $target = DIRECTORY_SEPARATOR . $this->getTarget($uri); + // Get the files repository directory. + $repository = realpath($this->getDirectoryPath()); + // Get the target directory. + $target_dir = realpath(dirname($repository . $target)) . DIRECTORY_SEPARATOR; + // Get the target name, without any directory components. + $target_name = drupal_basename($repository . $target); + // A directory component can point outside its parent directory if the path + // separator ('/' or '\') is followed by '..' to reference the parent + // directory. + $pattern = '@(/|\\\\)\.\.@'; + // Check whether the target can possibly point outside its parent. + $traversal = preg_match($pattern, $target); + // Check whether the target dir exists within the files repository. + $subdirectory = strpos($target_dir, $repository . DIRECTORY_SEPARATOR) === 0; + if ($traversal && !$subdirectory) { + // If the target path contains directory-traversal parts such as '/..' and + // does not resolve to a subdirectory of the repository, then return FALSE + // to avoid a possible exploit. return FALSE; } - return $realpath; + return $target_dir . $target_name; } /** diff --git a/core/modules/system/src/Tests/File/DirectoryTest.php b/core/modules/system/src/Tests/File/DirectoryTest.php index 158be71..fdfc361 100644 --- a/core/modules/system/src/Tests/File/DirectoryTest.php +++ b/core/modules/system/src/Tests/File/DirectoryTest.php @@ -161,4 +161,35 @@ function testFileDirectoryTemp() { $this->assertEqual(empty($tmp_directory), FALSE, 'file_directory_temp() returned a non-empty value.'); $this->assertEqual($config->get('path.temporary'), $tmp_directory); } -} + + /** + * Tests that symlinks are supported within the files directory. + */ + function testFileDirectorySymlinks() { + // The return value of file_directory_temp() should be outside + // the public files directory. + $temp = file_directory_temp(); + $public = drupal_realpath('public://'); + $dirname = $this->randomMachineName(20); + $filename = $this->randomMachineName(20); + // Create a randomly-named directory in the temp folder. + $temp_dir = drupal_realpath($temp) . DIRECTORY_SEPARATOR . $dirname; + drupal_mkdir($temp_dir); + // Create a symlink in the public files folder pointing to the + // newly-created directory. + $symlink = $public . DIRECTORY_SEPARATOR . $dirname; + symlink($temp_dir, $symlink); + // Copy a test file to the symlinked directory. + $source = 'core/misc/druplicon.png'; + $destination = "public://$dirname/$filename"; + file_unmanaged_copy($source, $destination, FILE_EXISTS_ERROR); + // Test that the real path of the copied file lies in the temp folder. + $realpath = drupal_realpath($destination); + $compare = $temp_dir . DIRECTORY_SEPARATOR . $filename; + $this->assertEqual($realpath, $compare, "drupal_realpath('$destination') returned '$realpath'; expected '$compare'."); + // Clean up the mess. + file_unmanaged_delete($destination); + drupal_unlink($symlink); + drupal_rmdir($temp_dir); + } +} \ No newline at end of file