5 * Contains \Drupal\security_review\Checks\FilePermissions.
8 namespace Drupal\security_review\Checks;
10 use Drupal\Core\StreamWrapper\PrivateStream;
11 use Drupal\Core\StreamWrapper\PublicStream;
13 use Drupal\security_review\Check;
14 use Drupal\security_review\CheckResult;
17 * Check that files aren't writeable by the server.
19 class FilePermissions extends Check {
24 public function getNamespace() {
25 return 'Security Review';
31 public function getTitle() {
32 return 'File permissions';
38 public function getMachineTitle() {
45 public function storesFindings() {
52 public function run($cli = FALSE) {
53 $result = CheckResult::SUCCESS;
55 $file_list = $this->getFileList('.');
56 $writable = $this->security()->findWritableFiles($file_list, $cli);
58 // Try creating or appending files.
59 // Assume it doesn't work.
60 $create_status = FALSE;
61 $append_status = FALSE;
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')
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");
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");
84 if (!empty($writable) || $create_status || $append_status) {
85 $result = CheckResult::FAIL;
88 return $this->createResult($result, $writable);
94 public function runCli() {
95 if (!$this->securityReview()->isServerPosix()) {
96 return $this->createResult(CheckResult::INFO);
99 return $this->run(TRUE);
105 public function help() {
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')
115 '#theme' => 'check_help',
116 '#title' => $this->t('Web server file system permissions'),
117 '#paragraphs' => $paragraphs,
124 public function evaluate(CheckResult $result) {
125 if ($result->result() == CheckResult::SUCCESS) {
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>.');
133 '#theme' => 'check_evaluation',
134 '#paragraphs' => $paragraphs,
135 '#items' => $result->findings(),
142 public function evaluatePlain(CheckResult $result) {
143 if ($result->result() == CheckResult::SUCCESS) {
147 $output = $this->t('Writable files:') . "\n";
148 foreach ($result->findings() as $file) {
149 $output .= "\t" . $file . "\n";
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.');
163 case CheckResult::FAIL:
164 return $this->t('Some files and directories in your install are writable by the server.');
166 case CheckResult::INFO:
167 return $this->t('The test cannot be run on this system.');
170 return $this->t('Unexpected result.');
175 * Scans a directory recursively and returns the files and directories inside.
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.
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)];
192 if ($ignore === NULL) {
193 $ignore = $this->getIgnoreList();
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));
207 $items[] = preg_replace("/\/\//si", "/", $path);
217 * Returns an array of relative and canonical paths to ignore.
220 * List of relative and canonical file paths to ignore.
222 protected function getIgnoreList() {
223 $file_path = PublicStream::basePath();
224 $ignore = ['..', 'CVS', '.git', '.svn', '.bzr', realpath($file_path)];
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, '/'));
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);
239 $ignore[] = $private_files;
242 $this->moduleHandler()->alter('security_review_file_ignore', $ignore);