Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
75.00% |
6 / 8 |
CRAP | |
64.00% |
32 / 50 |
| MTimeProtectedFastFileStorage | |
0.00% |
0 / 1 |
|
75.00% |
6 / 8 |
44.58 | |
64.00% |
32 / 50 |
| __construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
| save | |
0.00% |
0 / 1 |
4.01 | |
92.31% |
12 / 13 |
|||
| getFullPath | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
| delete | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| garbageCollection | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 17 |
|||
| getContainingDirectoryFullPath | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| getUncachedMTime | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| tempnam | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage. | |
| */ | |
| namespace Drupal\Component\PhpStorage; | |
| /** | |
| * Stores PHP code in files with securely hashed names. | |
| * | |
| * The goal of this class is to ensure that if a PHP file is replaced with | |
| * an untrusted one, it does not get loaded. Since mtime granularity is 1 | |
| * second, we cannot prevent an attack that happens within one second of the | |
| * initial save(). However, it is very unlikely for an attacker exploiting an | |
| * upload or file write vulnerability to also know when a legitimate file is | |
| * being saved, discover its hash, undo its file permissions, and override the | |
| * file with an upload all within a single second. Being able to accomplish | |
| * that would indicate a site very likely vulnerable to many other attack | |
| * vectors. | |
| * | |
| * Each file is stored in its own unique containing directory. The hash is based | |
| * on the virtual file name, the containing directory's mtime, and a | |
| * cryptographically hard to guess secret string. Thus, even if the hashed file | |
| * name is discovered and replaced by an untrusted file (e.g., via a | |
| * move_uploaded_file() invocation by a script that performs insufficient | |
| * validation), the directory's mtime gets updated in the process, invalidating | |
| * the hash and preventing the untrusted file from getting loaded. | |
| * | |
| * This class does not protect against overwriting a file in-place (e.g. a | |
| * malicious module that does a file_put_contents()) since this will not change | |
| * the mtime of the directory. MTimeProtectedFileStorage protects against this | |
| * at the cost of an additional system call for every load() and exists(). | |
| * | |
| * The containing directory is created with the same name as the virtual file | |
| * name (slashes removed) to assist with debugging, since the file itself is | |
| * stored with a name that's meaningless to humans. | |
| */ | |
| class MTimeProtectedFastFileStorage extends FileStorage { | |
| /** | |
| * The secret used in the HMAC. | |
| * | |
| * @var string | |
| */ | |
| protected $secret; | |
| /** | |
| * Constructs this MTimeProtectedFastFileStorage object. | |
| * | |
| * @param array $configuration | |
| * An associated array, containing at least these keys (the rest are | |
| * ignored): | |
| * - directory: The directory where the files should be stored. | |
| * - secret: A cryptographically hard to guess secret string. | |
| * -bin. The storage bin. Multiple storage objects can be instantiated with | |
| * the same configuration, but for different bins. | |
| */ | |
| public function __construct(array $configuration) { | |
| parent::__construct($configuration); | |
| $this->secret = $configuration['secret']; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function save($name, $data) { | |
| $this->ensureDirectory($this->directory); | |
| // Write the file out to a temporary location. Prepend with a '.' to keep it | |
| // hidden from listings and web servers. | |
| $temporary_path = $this->tempnam($this->directory, '.'); | |
| if (!$temporary_path || !@file_put_contents($temporary_path, $data)) { | |
| return FALSE; | |
| } | |
| // The file will not be chmod() in the future so this is the final | |
| // permission. | |
| chmod($temporary_path, 0444); | |
| // Determine the exact modification time of the file. | |
| $mtime = $this->getUncachedMTime($temporary_path); | |
| // Move the temporary file into the proper directory. Note that POSIX | |
| // compliant systems as well as modern Windows perform the rename operation | |
| // atomically, i.e. there is no point at which another process attempting to | |
| // access the new path will find it missing. | |
| $directory = $this->getContainingDirectoryFullPath($name); | |
| $this->ensureDirectory($directory); | |
| $full_path = $this->getFullPath($name, $directory, $mtime); | |
| $result = rename($temporary_path, $full_path); | |
| // Finally reset the modification time of the directory to match the one of | |
| // the newly created file. In order to prevent the creation of a file if the | |
| // directory does not exist, ensure that the path terminates with a | |
| // directory separator. | |
| // | |
| // Recall that when subsequently loading the file, the hash is calculated | |
| // based on the file name, the containing mtime, and a the secret string. | |
| // Hence updating the mtime here is comparable to pointing a symbolic link | |
| // at a new target, i.e., the newly created file. | |
| if ($result) { | |
| $result &= touch($directory . '/', $mtime); | |
| } | |
| return (bool) $result; | |
| } | |
| /** | |
| * Gets the full path where the file is or should be stored. | |
| * | |
| * This function creates a file path that includes a unique containing | |
| * directory for the file and a file name that is a hash of the virtual file | |
| * name, a cryptographic secret, and the containing directory mtime. If the | |
| * file is overridden by an insecure upload script, the directory mtime gets | |
| * modified, invalidating the file, thus protecting against untrusted code | |
| * getting executed. | |
| * | |
| * @param string $name | |
| * The virtual file name. Can be a relative path. | |
| * @param string $directory | |
| * (optional) The directory containing the file. If not passed, this is | |
| * retrieved by calling getContainingDirectoryFullPath(). | |
| * @param int $directory_mtime | |
| * (optional) The mtime of $directory. Can be passed to avoid an extra | |
| * filesystem call when the mtime of the directory is already known. | |
| * | |
| * @return string | |
| * The full path where the file is or should be stored. | |
| */ | |
| public function getFullPath($name, &$directory = NULL, &$directory_mtime = NULL) { | |
| if (!isset($directory)) { | |
| $directory = $this->getContainingDirectoryFullPath($name); | |
| } | |
| if (!isset($directory_mtime)) { | |
| $directory_mtime = file_exists($directory) ? filemtime($directory) : 0; | |
| } | |
| return $directory . '/' . hash_hmac('sha256', $name, $this->secret . $directory_mtime) . '.php'; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function delete($name) { | |
| $path = $this->getContainingDirectoryFullPath($name); | |
| if (file_exists($path)) { | |
| return $this->unlink($path); | |
| } | |
| return FALSE; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function garbageCollection() { | |
| $flags = \FilesystemIterator::CURRENT_AS_FILEINFO; | |
| $flags += \FilesystemIterator::SKIP_DOTS; | |
| foreach ($this->listAll() as $name) { | |
| $directory = $this->getContainingDirectoryFullPath($name); | |
| try { | |
| $dir_iterator = new \FilesystemIterator($directory, $flags); | |
| } | |
| catch (\UnexpectedValueException $e) { | |
| // FilesystemIterator throws an UnexpectedValueException if the | |
| // specified path is not a directory, or if it is not accessible. | |
| continue; | |
| } | |
| $directory_unlink = TRUE; | |
| $directory_mtime = filemtime($directory); | |
| foreach ($dir_iterator as $fileinfo) { | |
| if ($directory_mtime > $fileinfo->getMTime()) { | |
| // Ensure the folder is writable. | |
| @chmod($directory, 0777); | |
| @unlink($fileinfo->getPathName()); | |
| } | |
| else { | |
| // The directory still contains valid files. | |
| $directory_unlink = FALSE; | |
| } | |
| } | |
| if ($directory_unlink) { | |
| $this->unlink($name); | |
| } | |
| } | |
| } | |
| /** | |
| * Gets the full path of the containing directory where the file is or should | |
| * be stored. | |
| * | |
| * @param string $name | |
| * The virtual file name. Can be a relative path. | |
| * | |
| * @return string | |
| * The full path of the containing directory where the file is or should be | |
| * stored. | |
| */ | |
| protected function getContainingDirectoryFullPath($name) { | |
| // Remove the .php file extension from the directory name. | |
| // Within a single directory, a subdirectory cannot have the same name as a | |
| // file. Thus, when switching between MTimeProtectedFastFileStorage and | |
| // FileStorage, the subdirectory or the file cannot be created in case the | |
| // other file type exists already. | |
| if (substr($name, -4) === '.php') { | |
| $name = substr($name, 0, -4); | |
| } | |
| return $this->directory . '/' . str_replace('/', '#', $name); | |
| } | |
| /** | |
| * Clears PHP's stat cache and returns the directory's mtime. | |
| */ | |
| protected function getUncachedMTime($directory) { | |
| clearstatcache(TRUE, $directory); | |
| return filemtime($directory); | |
| } | |
| /** | |
| * A brute force tempnam implementation supporting streams. | |
| * | |
| * @param $directory | |
| * The directory where the temporary filename will be created. | |
| * @param $prefix | |
| * The prefix of the generated temporary filename. | |
| * @return string | |
| * Returns the new temporary filename (with path), or FALSE on failure. | |
| */ | |
| protected function tempnam($directory, $prefix) { | |
| do { | |
| $path = $directory . '/' . $prefix . substr(str_shuffle(hash('sha256', microtime())), 0, 10); | |
| } while (file_exists($path)); | |
| return $path; | |
| } | |
| } |