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