Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Check the presence of public paths via curl.
  19   *
  20   * @package    core
  21   * @category   check
  22   * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core\check\environment;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  use core\check\check;
  31  use core\check\result;
  32  
  33  /**
  34   * Check the public access of various paths.
  35   *
  36   * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class publicpaths extends check {
  40  
  41      /**
  42       * Get the short check name
  43       *
  44       * @return string
  45       */
  46      public function get_name(): string {
  47          return get_string('check_publicpaths_name', 'report_security');
  48      }
  49  
  50      /**
  51       * Returns a list of test urls and metadata.
  52       */
  53      public function get_pathsets() {
  54          global $CFG;
  55  
  56          // The intention here is that each pattern is a simple regex such that
  57          // in future perhaps the various webserver config could be generated as more
  58          // pattens are added to these checks.
  59          return [
  60              [
  61                  'pattern'   => '/vendor/',
  62                  '404'       => [
  63                      'vendor/',
  64                      'vendor/bin/behat',
  65                  ],
  66                  'details'   => get_string('check_vendordir_details', 'report_security', ['path' => $CFG->dirroot.'/vendor']),
  67                  'summary'   => get_string('check_vendordir_info', 'report_security'),
  68              ],
  69              [
  70                  'pattern'   => '/node_modules/',
  71                  '404'       => [
  72                      'node_modules/',
  73                      'node_modules/cli/cli.js',
  74                  ],
  75                  'summary'   => get_string('check_nodemodules_info', 'report_security'),
  76                  'details'   => get_string('check_nodemodules_details', 'report_security',
  77                          ['path' => $CFG->dirroot . '/node_modules']),
  78              ],
  79              [
  80                  'pattern'   => '^\..*',
  81                  '404'       => [
  82                      '.git/',
  83                      '.git/HEAD',
  84                      '.github/FUNDING.yml',
  85                      '.stylelintrc',
  86                  ],
  87              ],
  88              [
  89                  'pattern'   => 'composer.json',
  90                  '404'       => [
  91                      'composer.json',
  92                  ],
  93              ],
  94              [
  95                  'pattern'   => '.lock',
  96                  '404'       => [
  97                      'composer.lock',
  98                  ],
  99              ],
 100              [
 101                  'pattern'   => 'environment.xml',
 102                  '404'       => [
 103                      'admin/environment.xml',
 104                  ],
 105              ],
 106              [
 107                  'pattern'   => '',
 108                  '404'       => [
 109                      'doesnotexist', // Just to make sure that real 404s are still 404s.
 110                  ],
 111                  'summary'   => '',
 112              ],
 113              [
 114                  'pattern'   => '',
 115                  '404'       => [
 116                      'lib/classes/',
 117                  ],
 118                  'summary'   => get_string('check_dirindex_info', 'report_security'),
 119              ],
 120              [
 121                  'pattern'   => 'db/install.xml',
 122                  '404'       => [
 123                      'lib/db/install.xml',
 124                      'mod/assign/db/install.xml',
 125                  ],
 126              ],
 127              [
 128                  'pattern'   => 'readme.txt',
 129                  '404'       => [
 130                      'lib/scssphp/moodle_readme.txt',
 131                      'mod/resource/readme.txt',
 132                  ],
 133              ],
 134              [
 135                  'pattern'   => 'README',
 136                  '404'       => [
 137                      'mod/README.txt',
 138                      'mod/book/README.md',
 139                      'mod/chat/README.txt',
 140                  ],
 141              ],
 142              [
 143                  'pattern'   => '/upgrade.txt',
 144                  '404'       => [
 145                      'auth/manual/upgrade.txt',
 146                      'lib/upgrade.txt',
 147                  ],
 148              ],
 149              [
 150                  'pattern'   => 'phpunit.xml',
 151                  '404'       => ['phpunit.xml.dist'],
 152              ],
 153              [
 154                  'pattern'   => '/fixtures/',
 155                  '404'       => [
 156                      'privacy/tests/fixtures/logo.png',
 157                      'enrol/lti/tests/fixtures/input.xml',
 158                  ],
 159              ],
 160              [
 161                  'pattern'   => '/behat/',
 162                  '404'       => ['blog/tests/behat/delete.feature'],
 163              ],
 164          ];
 165      }
 166  
 167      /**
 168       * Return result
 169       * @return result
 170       */
 171      public function get_result(): result {
 172          global $CFG, $OUTPUT;
 173  
 174          $status = result::OK;
 175          $details = '';
 176          $summary = '';
 177          $errors = [];
 178  
 179          $c = new \curl();
 180          $paths = $this->get_pathsets();
 181  
 182          $table = new \html_table();
 183          $table->align = ['center', 'right', 'left'];
 184          $table->size = ['1%', '1%', '1%', '1%', '1%', '99%'];
 185          $table->head = [
 186              get_string('status'),
 187              get_string('checkexpected'),
 188              get_string('checkactual'),
 189              get_string('url'),
 190              get_string('category'),
 191              get_string('details'),
 192          ];
 193          $table->attributes['class'] = 'flexible generaltable generalbox table-sm';
 194          $table->data = [];
 195  
 196          // Used to track duplicated errors.
 197          $lastdetail = '-';
 198  
 199          $curl = new \curl();
 200          $requests = [];
 201  
 202          // Build up a list of all url so we can load them in parallel.
 203          foreach ($paths as $path) {
 204              foreach (['200', '404'] as $expected) {
 205                  if (!isset($path[$expected])) {
 206                      continue;
 207                  }
 208                  foreach ($path[$expected] as $test) {
 209                      $requests[] = [
 210                          'nobody'    => true,
 211                          'header'    => 1,
 212                          'url'       => $CFG->wwwroot . '/' . $test,
 213                          'returntransfer' => true,
 214                      ];
 215                  }
 216              }
 217          }
 218  
 219          $headers = $curl->download($requests);
 220  
 221          foreach ($paths as $path) {
 222              foreach (['200', '404'] as $expected) {
 223                  if (!isset($path[$expected])) {
 224                      continue;
 225                  }
 226                  foreach ($path[$expected] as $test) {
 227                      $rowsummary = '';
 228                      $rowdetail = '';
 229  
 230                      $url = $CFG->wwwroot . '/' . $test;
 231  
 232                      // Parse the HTTP header to get the 200 / 404 code.
 233                      $header = array_shift($headers);
 234                      $actual = strtok($header, "\n");
 235                      $actual = strtok($actual, " ");
 236                      $actual = strtok(" ");
 237  
 238                      if ($actual != $expected) {
 239                          if (isset($path['summary'])) {
 240                              $rowsummary = $path['summary'];
 241                          } else {
 242                              $rowsummary = get_string('check_publicpaths_generic',
 243                                  'report_security', $path['pattern']);
 244                          }
 245  
 246                          // Special case where a 404 is ideal but a 403 is ok too.
 247                          if ($actual == 403) {
 248                              $result = new result(result::INFO, '', '');
 249                              $rowsummary .= get_string('check_publicpaths_403', 'report_security');
 250                          } else {
 251                              $result = new result(result::ERROR, '', '');
 252                              $status = result::ERROR;
 253                          }
 254  
 255                          $rowdetail = isset($path['details']) ? $path['details'] : $rowsummary;
 256  
 257                          if (empty($errors[$path['pattern']])) {
 258                              $summary .= '<li>' . $rowsummary . '</li>';
 259                              $errors[$path['pattern']] = 1;
 260                          }
 261  
 262                      } else {
 263                          $result = new result(result::OK, '', '');
 264                      }
 265  
 266                      $table->data[] = [
 267                          $OUTPUT->check_result($result),
 268                          $expected,
 269                          $actual,
 270                          $OUTPUT->action_link($url, $test, null, ['target' => '_blank']),
 271                          "<pre>{$path['pattern']}</pre>",
 272                      ];
 273  
 274                      // Merge duplicate details to display a nicer table.
 275                      if ($rowdetail == $lastdetail) {
 276                          $duplicates++;
 277                      } else {
 278                          $duplicates = 1;
 279                      }
 280                      $detailcell = new \html_table_cell($rowdetail);
 281                      $detailcell->rowspan = $duplicates;
 282                      $rows = count($table->data);
 283                      $table->data[$rows - $duplicates][5] = $detailcell;
 284                      $lastdetail = $rowdetail;
 285                  }
 286              }
 287          }
 288  
 289          $details .= \html_writer::table($table);
 290  
 291          return new result($status, $summary, $details);
 292      }
 293  
 294      /**
 295       * Link to the dev docs for more info.
 296       *
 297       * @return action_link|null
 298       */
 299      public function get_action_link(): ?\action_link {
 300          return new \action_link(
 301              new \moodle_url(\get_docs_url('Installing_Moodle#Set_up_your_server')),
 302              get_string('moodledocs'));
 303      }
 304  
 305  }
 306