diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php index 54e7e7a..bbde233 100644 --- a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php @@ -162,17 +162,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 either 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/lib/Drupal/system/Tests/File/DirectoryTest.php b/core/modules/system/lib/Drupal/system/Tests/File/DirectoryTest.php index 9a7a812..910f6a1 100644 --- a/core/modules/system/lib/Drupal/system/Tests/File/DirectoryTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/File/DirectoryTest.php @@ -162,3 +162,40 @@ function testFileDirectoryTemp() { $this->assertEqual($config->get('path.temporary'), $tmp_directory); } } + + /** + * Tests that symlinks are supported within the files directory. + */ + function testFileDirectorySymlinks() { + // Temporarily unset the file_temporary_path variable. + $file_temporary_path = variable_get('file_temporary_path', NULL); + variable_del('file_temporary_path'); + // Now the return value of file_directory_temp() should be outside + // the public files directory. + $temp = file_directory_temp(); + // Restore the file_temporary_path variable. + variable_set('file_temporary_path', $file_temporary_path); + $public = drupal_realpath('public://'); + $dirname = $this->randomname(20); + $filename = $this->randomname(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 = current($this->drupalGetTestFiles('text'))->uri; + $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