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