Including security review as a submodule - with patched for Yaffs.
[yaffs-website] / web / modules / contrib / security_review / src / Checks / ExecutablePhp.php
1 <?php
2
3 /**
4  * @file
5  * Contains \Drupal\security_review\Checks\ExecutablePhp.
6  */
7
8 namespace Drupal\security_review\Checks;
9
10 use Drupal\Component\PhpStorage\FileStorage;
11 use Drupal\Core\StreamWrapper\PublicStream;
12 use Drupal\Core\Url;
13 use Drupal\security_review\Check;
14 use Drupal\security_review\CheckResult;
15 use GuzzleHttp\Exception\RequestException;
16
17 /**
18  * Checks if PHP files written to the files directory can be executed.
19  */
20 class ExecutablePhp extends Check {
21
22   /**
23    * Drupal's HTTP Client.
24    *
25    * @var \Drupal\Core\Http\Client
26    */
27   protected $httpClient;
28
29   /**
30    * {@inheritdoc}
31    */
32   public function __construct() {
33     parent::__construct();
34     $this->httpClient = $this->container->get('http_client');
35   }
36
37   /**
38    * {@inheritdoc}
39    */
40   public function getNamespace() {
41     return 'Security Review';
42   }
43
44   /**
45    * {@inheritdoc}
46    */
47   public function getTitle() {
48     return 'Executable PHP';
49   }
50
51   /**
52    * {@inheritdoc}
53    */
54   public function run($cli = FALSE) {
55     global $base_url;
56     $result = CheckResult::SUCCESS;
57     $findings = [];
58
59     // Set up test file data.
60     $message = 'Security review test ' . date('Ymdhis');
61     $content = "<?php\necho '" . $message . "';";
62     $file_path = PublicStream::basePath() . '/security_review_test.php';
63
64     // Create the test file.
65     if ($test_file = @fopen('./' . $file_path, 'w')) {
66       fwrite($test_file, $content);
67       fclose($test_file);
68     }
69
70     // Try to access the test file.
71     try {
72       $response = $this->httpClient->get($base_url . '/' . $file_path);
73       if ($response->getStatusCode() == 200 && $response->getBody() === $message) {
74         $result = CheckResult::FAIL;
75         $findings[] = 'executable_php';
76       }
77     }
78     catch (RequestException $e) {
79       // Access was denied to the file.
80     }
81
82     // Remove the test file.
83     if (file_exists('./' . $file_path)) {
84       @unlink('./' . $file_path);
85     }
86
87     // Check for presence of the .htaccess file and if the contents are correct.
88     $htaccess_path = PublicStream::basePath() . '/.htaccess';
89     if (!file_exists($htaccess_path)) {
90       $result = CheckResult::FAIL;
91       $findings[] = 'missing_htaccess';
92     }
93     else {
94       // Check whether the contents of .htaccess are correct.
95       $contents = file_get_contents($htaccess_path);
96       $expected = FileStorage::htaccessLines(FALSE);
97
98       // Trim each line separately then put them back together.
99       $contents = implode("\n", array_map('trim', explode("\n", trim($contents))));
100       $expected = implode("\n", array_map('trim', explode("\n", trim($expected))));
101
102       if ($contents !== $expected) {
103         $result = CheckResult::FAIL;
104         $findings[] = 'incorrect_htaccess';
105       }
106
107       // Check whether .htaccess is writable.
108       if (!$cli) {
109         $writable_htaccess = is_writable($htaccess_path);
110       }
111       else {
112         $writable = $this->security()->findWritableFiles([$htaccess_path], TRUE);
113         $writable_htaccess = !empty($writable);
114       }
115
116       if ($writable_htaccess) {
117         $findings[] = 'writable_htaccess';
118         if ($result !== CheckResult::FAIL) {
119           $result = CheckResult::WARN;
120         }
121       }
122     }
123
124     return $this->createResult($result, $findings);
125   }
126
127   /**
128    * {@inheritdoc}
129    */
130   public function runCli() {
131     return $this->run(TRUE);
132   }
133
134   /**
135    * {@inheritdoc}
136    */
137   public function help() {
138     $paragraphs = [];
139     $paragraphs[] = $this->t('The Drupal files directory is for user-uploaded files and by default provides some protection against a malicious user executing arbitrary PHP code against your site.');
140     $paragraphs[] = $this->t('Read more about the <a href="https://drupal.org/node/615888">risk of PHP code execution on Drupal.org</a>.');
141
142     return [
143       '#theme' => 'check_help',
144       '#title' => $this->t('Executable PHP in files directory'),
145       '#paragraphs' => $paragraphs,
146     ];
147   }
148
149   /**
150    * {@inheritdoc}
151    */
152   public function evaluate(CheckResult $result) {
153     $paragraphs = [];
154     foreach ($result->findings() as $label) {
155       switch ($label) {
156         case 'executable_php':
157           $paragraphs[] = $this->t('Security Review was able to execute a PHP file written to your files directory.');
158           break;
159
160         case 'missing_htaccess':
161           $directory = PublicStream::basePath();
162           $paragraphs[] = $this->t("The .htaccess file is missing from the files directory at @path", ['@path' => $directory]);
163           $paragraphs[] = $this->t("Note, if you are using a webserver other than Apache you should consult your server's documentation on how to limit the execution of PHP scripts in this directory.");
164           break;
165
166         case 'incorrect_htaccess':
167           $paragraphs[] = $this->t("The .htaccess file exists but does not contain the correct content. It is possible it's been maliciously altered.");
168           break;
169
170         case 'writable_htaccess':
171           $paragraphs[] = $this->t("The .htaccess file is writable which poses a risk should a malicious user find a way to execute PHP code they could alter the .htaccess file to allow further PHP code execution.");
172           break;
173       }
174     }
175
176     return [
177       '#theme' => 'check_evaluation',
178       '#paragraphs' => $paragraphs,
179       '#items' => [],
180     ];
181   }
182
183   /**
184    * {@inheritdoc}
185    */
186   public function evaluatePlain(CheckResult $result) {
187     $paragraphs = [];
188     $directory = PublicStream::basePath();
189     foreach ($result->findings() as $label) {
190       switch ($label) {
191         case 'executable_php':
192           $paragraphs[] = $this->t('PHP file executed in @path', ['@path' => $directory]);
193           break;
194
195         case 'missing_htaccess':
196           $paragraphs[] = $this->t('.htaccess is missing from @path', ['@path' => $directory]);
197           break;
198
199         case 'incorrect_htaccess':
200           $paragraphs[] = $this->t('.htaccess wrong content');
201           break;
202
203         case 'writable_htaccess':
204           $paragraphs[] = $this->t('.htaccess writable');
205           break;
206       }
207     }
208
209     return implode("\n", $paragraphs);
210   }
211
212   /**
213    * {@inheritdoc}
214    */
215   public function getMessage($result_const) {
216     switch ($result_const) {
217       case CheckResult::SUCCESS:
218         return $this->t('PHP files in the Drupal files directory cannot be executed.');
219
220       case CheckResult::FAIL:
221         return $this->t('PHP files in the Drupal files directory can be executed.');
222
223       case CheckResult::WARN:
224         return $this->t('The .htaccess file in the files directory is writable.');
225
226       default:
227         return $this->t('Unexpected result.');
228     }
229   }
230
231 }