PageRenderTime 41ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/core/lib/Drupal/Component/PhpStorage/MTimeProtectedFastFileStorage.php

http://github.com/drupal/drupal
PHP | 235 lines | 86 code | 21 blank | 128 comment | 11 complexity | 1facebb9d95cb3d043848ac4a227fe11 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\Component\PhpStorage;
  3. use Drupal\Component\Utility\Crypt;
  4. /**
  5. * Stores PHP code in files with securely hashed names.
  6. *
  7. * The goal of this class is to ensure that if a PHP file is replaced with
  8. * an untrusted one, it does not get loaded. Since mtime granularity is 1
  9. * second, we cannot prevent an attack that happens within one second of the
  10. * initial save(). However, it is very unlikely for an attacker exploiting an
  11. * upload or file write vulnerability to also know when a legitimate file is
  12. * being saved, discover its hash, undo its file permissions, and override the
  13. * file with an upload all within a single second. Being able to accomplish
  14. * that would indicate a site very likely vulnerable to many other attack
  15. * vectors.
  16. *
  17. * Each file is stored in its own unique containing directory. The hash is based
  18. * on the virtual file name, the containing directory's mtime, and a
  19. * cryptographically hard to guess secret string. Thus, even if the hashed file
  20. * name is discovered and replaced by an untrusted file (e.g., via a
  21. * move_uploaded_file() invocation by a script that performs insufficient
  22. * validation), the directory's mtime gets updated in the process, invalidating
  23. * the hash and preventing the untrusted file from getting loaded.
  24. *
  25. * This class does not protect against overwriting a file in-place (e.g. a
  26. * malicious module that does a file_put_contents()) since this will not change
  27. * the mtime of the directory. MTimeProtectedFileStorage protects against this
  28. * at the cost of an additional system call for every load() and exists().
  29. *
  30. * The containing directory is created with the same name as the virtual file
  31. * name (slashes removed) to assist with debugging, since the file itself is
  32. * stored with a name that's meaningless to humans.
  33. */
  34. class MTimeProtectedFastFileStorage extends FileStorage {
  35. /**
  36. * The secret used in the HMAC.
  37. *
  38. * @var string
  39. */
  40. protected $secret;
  41. /**
  42. * Constructs this MTimeProtectedFastFileStorage object.
  43. *
  44. * @param array $configuration
  45. * An associated array, containing at least these keys (the rest are
  46. * ignored):
  47. * - directory: The directory where the files should be stored.
  48. * - secret: A cryptographically hard to guess secret string.
  49. * -bin. The storage bin. Multiple storage objects can be instantiated with
  50. * the same configuration, but for different bins.
  51. */
  52. public function __construct(array $configuration) {
  53. parent::__construct($configuration);
  54. $this->secret = $configuration['secret'];
  55. }
  56. /**
  57. * {@inheritdoc}
  58. */
  59. public function save($name, $data) {
  60. $this->ensureDirectory($this->directory);
  61. // Write the file out to a temporary location. Prepend with a '.' to keep it
  62. // hidden from listings and web servers.
  63. $temporary_path = $this->tempnam($this->directory, '.');
  64. if (!$temporary_path || !@file_put_contents($temporary_path, $data)) {
  65. return FALSE;
  66. }
  67. // The file will not be chmod() in the future so this is the final
  68. // permission.
  69. chmod($temporary_path, 0444);
  70. // Determine the exact modification time of the file.
  71. $mtime = $this->getUncachedMTime($temporary_path);
  72. // Move the temporary file into the proper directory. Note that POSIX
  73. // compliant systems as well as modern Windows perform the rename operation
  74. // atomically, i.e. there is no point at which another process attempting to
  75. // access the new path will find it missing.
  76. $directory = $this->getContainingDirectoryFullPath($name);
  77. $this->ensureDirectory($directory);
  78. $full_path = $this->getFullPath($name, $directory, $mtime);
  79. $result = rename($temporary_path, $full_path);
  80. // Finally reset the modification time of the directory to match the one of
  81. // the newly created file. In order to prevent the creation of a file if the
  82. // directory does not exist, ensure that the path terminates with a
  83. // directory separator.
  84. //
  85. // Recall that when subsequently loading the file, the hash is calculated
  86. // based on the file name, the containing mtime, and a the secret string.
  87. // Hence updating the mtime here is comparable to pointing a symbolic link
  88. // at a new target, i.e., the newly created file.
  89. if ($result) {
  90. $result &= touch($directory . '/', $mtime);
  91. }
  92. return (bool) $result;
  93. }
  94. /**
  95. * Gets the full path where the file is or should be stored.
  96. *
  97. * This function creates a file path that includes a unique containing
  98. * directory for the file and a file name that is a hash of the virtual file
  99. * name, a cryptographic secret, and the containing directory mtime. If the
  100. * file is overridden by an insecure upload script, the directory mtime gets
  101. * modified, invalidating the file, thus protecting against untrusted code
  102. * getting executed.
  103. *
  104. * @param string $name
  105. * The virtual file name. Can be a relative path.
  106. * @param string $directory
  107. * (optional) The directory containing the file. If not passed, this is
  108. * retrieved by calling getContainingDirectoryFullPath().
  109. * @param int $directory_mtime
  110. * (optional) The mtime of $directory. Can be passed to avoid an extra
  111. * filesystem call when the mtime of the directory is already known.
  112. *
  113. * @return string
  114. * The full path where the file is or should be stored.
  115. */
  116. public function getFullPath($name, &$directory = NULL, &$directory_mtime = NULL) {
  117. if (!isset($directory)) {
  118. $directory = $this->getContainingDirectoryFullPath($name);
  119. }
  120. if (!isset($directory_mtime)) {
  121. $directory_mtime = file_exists($directory) ? filemtime($directory) : 0;
  122. }
  123. return $directory . '/' . Crypt::hmacBase64($name, $this->secret . $directory_mtime) . '.php';
  124. }
  125. /**
  126. * {@inheritdoc}
  127. */
  128. public function delete($name) {
  129. $path = $this->getContainingDirectoryFullPath($name);
  130. if (file_exists($path)) {
  131. return $this->unlink($path);
  132. }
  133. return FALSE;
  134. }
  135. /**
  136. * {@inheritdoc}
  137. */
  138. public function garbageCollection() {
  139. $flags = \FilesystemIterator::CURRENT_AS_FILEINFO;
  140. $flags += \FilesystemIterator::SKIP_DOTS;
  141. foreach ($this->listAll() as $name) {
  142. $directory = $this->getContainingDirectoryFullPath($name);
  143. try {
  144. $dir_iterator = new \FilesystemIterator($directory, $flags);
  145. }
  146. catch (\UnexpectedValueException $e) {
  147. // FilesystemIterator throws an UnexpectedValueException if the
  148. // specified path is not a directory, or if it is not accessible.
  149. continue;
  150. }
  151. $directory_unlink = TRUE;
  152. $directory_mtime = filemtime($directory);
  153. foreach ($dir_iterator as $fileinfo) {
  154. if ($directory_mtime > $fileinfo->getMTime()) {
  155. // Ensure the folder is writable.
  156. @chmod($directory, 0777);
  157. @unlink($fileinfo->getPathName());
  158. }
  159. else {
  160. // The directory still contains valid files.
  161. $directory_unlink = FALSE;
  162. }
  163. }
  164. if ($directory_unlink) {
  165. $this->unlink($name);
  166. }
  167. }
  168. }
  169. /**
  170. * Gets the full path of the containing directory where the file is or should
  171. * be stored.
  172. *
  173. * @param string $name
  174. * The virtual file name. Can be a relative path.
  175. *
  176. * @return string
  177. * The full path of the containing directory where the file is or should be
  178. * stored.
  179. */
  180. protected function getContainingDirectoryFullPath($name) {
  181. // Remove the .php file extension from the directory name.
  182. // Within a single directory, a subdirectory cannot have the same name as a
  183. // file. Thus, when switching between MTimeProtectedFastFileStorage and
  184. // FileStorage, the subdirectory or the file cannot be created in case the
  185. // other file type exists already.
  186. if (substr($name, -4) === '.php') {
  187. $name = substr($name, 0, -4);
  188. }
  189. return $this->directory . '/' . str_replace('/', '#', $name);
  190. }
  191. /**
  192. * Clears PHP's stat cache and returns the directory's mtime.
  193. */
  194. protected function getUncachedMTime($directory) {
  195. clearstatcache(TRUE, $directory);
  196. return filemtime($directory);
  197. }
  198. /**
  199. * A brute force tempnam implementation supporting streams.
  200. *
  201. * @param $directory
  202. * The directory where the temporary filename will be created.
  203. * @param $prefix
  204. * The prefix of the generated temporary filename.
  205. * @return string
  206. * Returns the new temporary filename (with path), or FALSE on failure.
  207. */
  208. protected function tempnam($directory, $prefix) {
  209. do {
  210. $path = $directory . '/' . $prefix . Crypt::randomBytesBase64(20);
  211. } while (file_exists($path));
  212. return $path;
  213. }
  214. }