3 namespace Drupal\security_review\Checks;
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;
12 * Checks if PHP files written to the files directory can be executed.
14 class ExecutablePhp extends Check {
17 * Drupal's HTTP Client.
19 * @var \Drupal\Core\Http\Client
21 protected $httpClient;
26 public function __construct() {
27 parent::__construct();
28 $this->httpClient = $this->container->get('http_client');
34 public function getNamespace() {
35 return 'Security Review';
41 public function getTitle() {
42 return 'Executable PHP';
48 public function run($cli = FALSE) {
50 $result = CheckResult::SUCCESS;
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';
58 // Create the test file.
59 if ($test_file = @fopen('./' . $file_path, 'w')) {
60 fwrite($test_file, $content);
64 // Try to access the test file.
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';
72 catch (RequestException $e) {
73 // Access was denied to the file.
76 // Remove the test file.
77 if (file_exists('./' . $file_path)) {
78 @unlink('./' . $file_path);
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';
88 // Check whether the contents of .htaccess are correct.
89 $contents = file_get_contents($htaccess_path);
90 $expected = FileStorage::htaccessLines(FALSE);
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))));
96 if ($contents !== $expected) {
97 $result = CheckResult::FAIL;
98 $findings[] = 'incorrect_htaccess';
101 // Check whether .htaccess is writable.
103 $writable_htaccess = is_writable($htaccess_path);
106 $writable = $this->security()->findWritableFiles([$htaccess_path], TRUE);
107 $writable_htaccess = !empty($writable);
110 if ($writable_htaccess) {
111 $findings[] = 'writable_htaccess';
112 if ($result !== CheckResult::FAIL) {
113 $result = CheckResult::WARN;
118 return $this->createResult($result, $findings);
124 public function runCli() {
125 return $this->run(TRUE);
131 public function help() {
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>.');
137 '#theme' => 'check_help',
138 '#title' => $this->t('Executable PHP in files directory'),
139 '#paragraphs' => $paragraphs,
146 public function evaluate(CheckResult $result) {
148 foreach ($result->findings() as $label) {
150 case 'executable_php':
151 $paragraphs[] = $this->t('Security Review was able to execute a PHP file written to your files directory.');
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.");
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.");
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.");
171 '#theme' => 'check_evaluation',
172 '#paragraphs' => $paragraphs,
180 public function evaluatePlain(CheckResult $result) {
182 $directory = PublicStream::basePath();
183 foreach ($result->findings() as $label) {
185 case 'executable_php':
186 $paragraphs[] = $this->t('PHP file executed in @path', ['@path' => $directory]);
189 case 'missing_htaccess':
190 $paragraphs[] = $this->t('.htaccess is missing from @path', ['@path' => $directory]);
193 case 'incorrect_htaccess':
194 $paragraphs[] = $this->t('.htaccess wrong content');
197 case 'writable_htaccess':
198 $paragraphs[] = $this->t('.htaccess writable');
203 return implode("\n", $paragraphs);
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.');
214 case CheckResult::FAIL:
215 return $this->t('PHP files in the Drupal files directory can be executed.');
217 case CheckResult::WARN:
218 return $this->t('The .htaccess file in the files directory is writable.');
221 return $this->t('Unexpected result.');