5 * Contains \Drupal\security_review\Checks\ExecutablePhp.
8 namespace Drupal\security_review\Checks;
10 use Drupal\Component\PhpStorage\FileStorage;
11 use Drupal\Core\StreamWrapper\PublicStream;
13 use Drupal\security_review\Check;
14 use Drupal\security_review\CheckResult;
15 use GuzzleHttp\Exception\RequestException;
18 * Checks if PHP files written to the files directory can be executed.
20 class ExecutablePhp extends Check {
23 * Drupal's HTTP Client.
25 * @var \Drupal\Core\Http\Client
27 protected $httpClient;
32 public function __construct() {
33 parent::__construct();
34 $this->httpClient = $this->container->get('http_client');
40 public function getNamespace() {
41 return 'Security Review';
47 public function getTitle() {
48 return 'Executable PHP';
54 public function run($cli = FALSE) {
56 $result = CheckResult::SUCCESS;
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';
64 // Create the test file.
65 if ($test_file = @fopen('./' . $file_path, 'w')) {
66 fwrite($test_file, $content);
70 // Try to access the test file.
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';
78 catch (RequestException $e) {
79 // Access was denied to the file.
82 // Remove the test file.
83 if (file_exists('./' . $file_path)) {
84 @unlink('./' . $file_path);
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';
94 // Check whether the contents of .htaccess are correct.
95 $contents = file_get_contents($htaccess_path);
96 $expected = FileStorage::htaccessLines(FALSE);
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))));
102 if ($contents !== $expected) {
103 $result = CheckResult::FAIL;
104 $findings[] = 'incorrect_htaccess';
107 // Check whether .htaccess is writable.
109 $writable_htaccess = is_writable($htaccess_path);
112 $writable = $this->security()->findWritableFiles([$htaccess_path], TRUE);
113 $writable_htaccess = !empty($writable);
116 if ($writable_htaccess) {
117 $findings[] = 'writable_htaccess';
118 if ($result !== CheckResult::FAIL) {
119 $result = CheckResult::WARN;
124 return $this->createResult($result, $findings);
130 public function runCli() {
131 return $this->run(TRUE);
137 public function help() {
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>.');
143 '#theme' => 'check_help',
144 '#title' => $this->t('Executable PHP in files directory'),
145 '#paragraphs' => $paragraphs,
152 public function evaluate(CheckResult $result) {
154 foreach ($result->findings() as $label) {
156 case 'executable_php':
157 $paragraphs[] = $this->t('Security Review was able to execute a PHP file written to your files directory.');
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.");
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.");
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.");
177 '#theme' => 'check_evaluation',
178 '#paragraphs' => $paragraphs,
186 public function evaluatePlain(CheckResult $result) {
188 $directory = PublicStream::basePath();
189 foreach ($result->findings() as $label) {
191 case 'executable_php':
192 $paragraphs[] = $this->t('PHP file executed in @path', ['@path' => $directory]);
195 case 'missing_htaccess':
196 $paragraphs[] = $this->t('.htaccess is missing from @path', ['@path' => $directory]);
199 case 'incorrect_htaccess':
200 $paragraphs[] = $this->t('.htaccess wrong content');
203 case 'writable_htaccess':
204 $paragraphs[] = $this->t('.htaccess writable');
209 return implode("\n", $paragraphs);
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.');
220 case CheckResult::FAIL:
221 return $this->t('PHP files in the Drupal files directory can be executed.');
223 case CheckResult::WARN:
224 return $this->t('The .htaccess file in the files directory is writable.');
227 return $this->t('Unexpected result.');