Including security review as a submodule - with patched for Yaffs.
[yaffs-website] / web / modules / contrib / security_review / src / Checks / FilePermissions.php
1 <?php
2
3 /**
4  * @file
5  * Contains \Drupal\security_review\Checks\FilePermissions.
6  */
7
8 namespace Drupal\security_review\Checks;
9
10 use Drupal\Core\StreamWrapper\PrivateStream;
11 use Drupal\Core\StreamWrapper\PublicStream;
12 use Drupal\Core\Url;
13 use Drupal\security_review\Check;
14 use Drupal\security_review\CheckResult;
15
16 /**
17  * Check that files aren't writeable by the server.
18  */
19 class FilePermissions extends Check {
20
21   /**
22    * {@inheritdoc}
23    */
24   public function getNamespace() {
25     return 'Security Review';
26   }
27
28   /**
29    * {@inheritdoc}
30    */
31   public function getTitle() {
32     return 'File permissions';
33   }
34
35   /**
36    * {@inheritdoc}
37    */
38   public function getMachineTitle() {
39     return 'file_perms';
40   }
41
42   /**
43    * {@inheritdoc}
44    */
45   public function storesFindings() {
46     return FALSE;
47   }
48
49   /**
50    * {@inheritdoc}
51    */
52   public function run($cli = FALSE) {
53     $result = CheckResult::SUCCESS;
54
55     $file_list = $this->getFileList('.');
56     $writable = $this->security()->findWritableFiles($file_list, $cli);
57
58     // Try creating or appending files.
59     // Assume it doesn't work.
60     $create_status = FALSE;
61     $append_status = FALSE;
62
63     if (!$cli) {
64       $append_message = $this->t("Your web server should not be able to write to your modules directory. This is a security vulnerable. Consult the Security Review file permissions check help for mitigation steps.");
65       $directory = $this->moduleHandler()
66         ->getModule('security_review')
67         ->getPath();
68
69       // Write a file with the timestamp.
70       $file = './' . $directory . '/file_write_test.' . date('Ymdhis');
71       if ($file_create = @fopen($file, 'w')) {
72         $create_status = fwrite($file_create, date('Ymdhis') . ' - ' . $append_message . "\n");
73         fclose($file_create);
74       }
75
76       // Try to append to our IGNOREME file.
77       $file = './' . $directory . '/IGNOREME.txt';
78       if ($file_append = @fopen($file, 'a')) {
79         $append_status = fwrite($file_append, date('Ymdhis') . ' - ' . $append_message . "\n");
80         fclose($file_append);
81       }
82     }
83
84     if (!empty($writable) || $create_status || $append_status) {
85       $result = CheckResult::FAIL;
86     }
87
88     return $this->createResult($result, $writable);
89   }
90
91   /**
92    * {@inheritdoc}
93    */
94   public function runCli() {
95     if (!$this->securityReview()->isServerPosix()) {
96       return $this->createResult(CheckResult::INFO);
97     }
98
99     return $this->run(TRUE);
100   }
101
102   /**
103    * {@inheritdoc}
104    */
105   public function help() {
106     $paragraphs = [];
107     $paragraphs[] = $this->t('It is dangerous to allow the web server to write to files inside the document root of your server. Doing so could allow Drupal to write files that could then be executed. An attacker might use such a vulnerability to take control of your site. An exception is the Drupal files, private files, and temporary directories which Drupal needs permission to write to in order to provide features like file attachments.');
108     $paragraphs[] = $this->t('In addition to inspecting existing directories, this test attempts to create and write to your file system. Look in your security_review module directory on the server for files named file_write_test.YYYYMMDDHHMMSS and for a file called IGNOREME.txt which gets a timestamp appended to it if it is writeable.');
109     $paragraphs[] = $this->l(
110       $this->t('Read more about file system permissions in the handbooks.'),
111       Url::fromUri('http://drupal.org/node/244924')
112     );
113
114     return [
115       '#theme' => 'check_help',
116       '#title' => $this->t('Web server file system permissions'),
117       '#paragraphs' => $paragraphs,
118     ];
119   }
120
121   /**
122    * {@inheritdoc}
123    */
124   public function evaluate(CheckResult $result) {
125     if ($result->result() == CheckResult::SUCCESS) {
126       return [];
127     }
128
129     $paragraphs = [];
130     $paragraphs[] = $this->t('The following files and directories appear to be writeable by your web server. In most cases you can fix this by simply altering the file permissions or ownership. If you have command-line access to your host try running "chmod 644 [file path]" where [file path] is one of the following paths (relative to your webroot). For more information consult the <a href="http://drupal.org/node/244924">Drupal.org handbooks on file permissions</a>.');
131
132     return [
133       '#theme' => 'check_evaluation',
134       '#paragraphs' => $paragraphs,
135       '#items' => $result->findings(),
136     ];
137   }
138
139   /**
140    * {@inheritdoc}
141    */
142   public function evaluatePlain(CheckResult $result) {
143     if ($result->result() == CheckResult::SUCCESS) {
144       return '';
145     }
146
147     $output = $this->t('Writable files:') . "\n";
148     foreach ($result->findings() as $file) {
149       $output .= "\t" . $file . "\n";
150     }
151
152     return $output;
153   }
154
155   /**
156    * {@inheritdoc}
157    */
158   public function getMessage($result_const) {
159     switch ($result_const) {
160       case CheckResult::SUCCESS:
161         return $this->t('Drupal installation files and directories (except required) are not writable by the server.');
162
163       case CheckResult::FAIL:
164         return $this->t('Some files and directories in your install are writable by the server.');
165
166       case CheckResult::INFO:
167         return $this->t('The test cannot be run on this system.');
168
169       default:
170         return $this->t('Unexpected result.');
171     }
172   }
173
174   /**
175    * Scans a directory recursively and returns the files and directories inside.
176    *
177    * @param string $directory
178    *   The directory to scan.
179    * @param string[] $parsed
180    *   Array of already parsed real paths.
181    * @param string[] $ignore
182    *   Array of file names to ignore.
183    *
184    * @return string[]
185    *   The items found.
186    */
187   protected function getFileList($directory, array &$parsed = NULL, array &$ignore = NULL) {
188     // Initialize $parsed and $ignore arrays.
189     if ($parsed === NULL) {
190       $parsed = [realpath($directory)];
191     }
192     if ($ignore === NULL) {
193       $ignore = $this->getIgnoreList();
194     }
195
196     // Start scanning.
197     $items = [];
198     if ($handle = opendir($directory)) {
199       while (($file = readdir($handle)) !== FALSE) {
200         // Don't check hidden files or ones we said to ignore.
201         $path = $directory . "/" . $file;
202         if ($file[0] != "." && !in_array($file, $ignore) && !in_array(realpath($path), $ignore)) {
203           if (is_dir($path) && !in_array(realpath($path), $parsed)) {
204             $parsed[] = realpath($path);
205             $items = array_merge($items, $this->getFileList($path, $parsed, $ignore));
206           }
207           $items[] = preg_replace("/\/\//si", "/", $path);
208         }
209       }
210       closedir($handle);
211     }
212
213     return $items;
214   }
215
216   /**
217    * Returns an array of relative and canonical paths to ignore.
218    *
219    * @return string[]
220    *   List of relative and canonical file paths to ignore.
221    */
222   protected function getIgnoreList() {
223     $file_path = PublicStream::basePath();
224     $ignore = ['..', 'CVS', '.git', '.svn', '.bzr', realpath($file_path)];
225
226     // Add temporary files directory if it's set.
227     $temp_path = file_directory_temp();
228     if (!empty($temp_path)) {
229       $ignore[] = realpath('./' . rtrim($temp_path, '/'));
230     }
231
232     // Add private files directory if it's set.
233     $private_files = PrivateStream::basePath();
234     if (!empty($private_files)) {
235       // Remove leading slash if set.
236       if (strrpos($private_files, '/') !== FALSE) {
237         $private_files = substr($private_files, strrpos($private_files, '/') + 1);
238       }
239       $ignore[] = $private_files;
240     }
241
242     $this->moduleHandler()->alter('security_review_file_ignore', $ignore);
243     return $ignore;
244   }
245
246 }