Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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  namespace core;
  18  
  19  /**
  20   * cURL security test suite.
  21   *
  22   * Note: The curl_security_helper class performs forward and reverse DNS look-ups in some cases. This class will not attempt to test
  23   * this functionality as look-ups can vary from machine to machine. Instead, human testing with known inputs/outputs is recommended.
  24   *
  25   * @package    core
  26   * @copyright  2016 Jake Dallimore <jrhdallimore@gmail.com>
  27   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  class curl_security_helper_test extends \advanced_testcase {
  30      /**
  31       * Test for \core\files\curl_security_helper::url_is_blocked().
  32       *
  33       * @param array $dns a mapping between hosts and IPs to be used instead of a real DNS lookup. The values must be arrays.
  34       * @param string $url the url to validate.
  35       * @param string $blockedhosts the list of blocked hosts.
  36       * @param string $allowedports the list of allowed ports.
  37       * @param bool $expected the expected result.
  38       * @dataProvider curl_security_url_data_provider
  39       */
  40      public function test_curl_security_helper_url_is_blocked($dns, $url, $blockedhosts, $allowedports, $expected) {
  41          $this->resetAfterTest(true);
  42          $helper = $this->getMockBuilder('\core\files\curl_security_helper')
  43              ->onlyMethods(['get_host_list_by_name'])
  44              ->getMock();
  45  
  46          // Override the get host list method to return hard coded values based on a mapping provided by $dns.
  47          $helper->method('get_host_list_by_name')->will(
  48              $this->returnCallback(
  49                  function($host) use ($dns) {
  50                      return isset($dns[$host]) ? $dns[$host] : [];
  51                  }
  52              )
  53          );
  54  
  55          set_config('curlsecurityblockedhosts', $blockedhosts);
  56          set_config('curlsecurityallowedport', $allowedports);
  57          $this->assertEquals($expected, $helper->url_is_blocked($url));
  58      }
  59  
  60      /**
  61       * Data provider for test_curl_security_helper_url_is_blocked().
  62       *
  63       * @return array
  64       */
  65      public function curl_security_url_data_provider() {
  66          $simpledns = ['localhost' => ['127.0.0.1']];
  67          $multiplerecorddns = [
  68              'sub.example.com' => ['1.2.3.4', '5.6.7.8']
  69          ];
  70          // Format: url, blocked hosts, allowed ports, expected result.
  71          return [
  72              // Base set without the blocklist enabled - no checking takes place.
  73              [$simpledns, "http://localhost/x.png", "", "", false],       // IP=127.0.0.1, Port=80 (port inferred from http).
  74              [$simpledns, "http://localhost:80/x.png", "", "", false],    // IP=127.0.0.1, Port=80 (specific port overrides http scheme).
  75              [$simpledns, "https://localhost/x.png", "", "", false],      // IP=127.0.0.1, Port=443 (port inferred from https).
  76              [$simpledns, "http://localhost:443/x.png", "", "", false],   // IP=127.0.0.1, Port=443 (specific port overrides http scheme).
  77              [$simpledns, "localhost/x.png", "", "", false],              // IP=127.0.0.1, Port=80 (port inferred from http fallback).
  78              [$simpledns, "localhost:443/x.png", "", "", false],          // IP=127.0.0.1, Port=443 (port hard specified, despite http fallback).
  79              [$simpledns, "http://127.0.0.1/x.png", "", "", false],       // IP=127.0.0.1, Port=80 (port inferred from http).
  80              [$simpledns, "127.0.0.1/x.png", "", "", false],              // IP=127.0.0.1, Port=80 (port inferred from http fallback).
  81              [$simpledns, "http://localhost:8080/x.png", "", "", false],  // IP=127.0.0.1, Port=8080 (port hard specified).
  82              [$simpledns, "http://192.168.1.10/x.png", "", "", false],    // IP=192.168.1.10, Port=80 (port inferred from http).
  83              [$simpledns, "https://192.168.1.10/x.png", "", "", false],   // IP=192.168.1.10, Port=443 (port inferred from https).
  84              [$simpledns, "http://sub.example.com/x.png", "", "", false], // IP=::1, Port = 80 (port inferred from http).
  85              [$simpledns, "http://s-1.d-1.com/x.png", "", "", false],     // IP=::1, Port = 80 (port inferred from http).
  86  
  87              // Test set using domain name filters but with all ports allowed (empty).
  88              [$simpledns, "http://localhost/x.png", "localhost", "", true],
  89              [$simpledns, "localhost/x.png", "localhost", "", true],
  90              [$simpledns, "localhost:0/x.png", "localhost", "", true],
  91              [$simpledns, "ftp://localhost/x.png", "localhost", "", true],
  92              [$simpledns, "http://sub.example.com/x.png", "localhost", "", false],
  93              [$simpledns, "http://example.com/x.png", "example.com", "", true],
  94              [$simpledns, "http://sub.example.com/x.png", "example.com", "", false],
  95  
  96              // Test set using wildcard domain name filters but with all ports allowed (empty).
  97              [$simpledns, "http://sub.example.com/x.png", "*.com", "", true],
  98              [$simpledns, "http://example.com/x.png", "*.example.com", "", false],
  99              [$simpledns, "http://sub.example.com/x.png", "*.example.com", "", true],
 100              [$simpledns, "http://sub.example.com/x.png", "*.sub.example.com", "", false],
 101              [$simpledns, "http://sub.example.com/x.png", "*.example", "", false],
 102  
 103              // Test set using IP address filters but with all ports allowed (empty).
 104              [$simpledns, "http://localhost/x.png", "127.0.0.1", "", true],
 105              [$simpledns, "http://127.0.0.1/x.png", "127.0.0.1", "", true],
 106  
 107              // Test set using CIDR IP range filters but with all ports allowed (empty).
 108              [$simpledns, "http://localhost/x.png", "127.0.0.0/24", "", true],
 109              [$simpledns, "http://127.0.0.1/x.png", "127.0.0.0/24", "", true],
 110  
 111              // Test set using last-group range filters but with all ports allowed (empty).
 112              [$simpledns, "http://localhost/x.png", "127.0.0.0-30", "", true],
 113              [$simpledns, "http://127.0.0.1/x.png", "127.0.0.0-30", "", true],
 114  
 115              // Test set using port filters but with all hosts allowed (empty).
 116              [$simpledns, "http://localhost/x.png", "", "80\n443", false],
 117              [$simpledns, "http://localhost:80/x.png", "", "80\n443", false],
 118              [$simpledns, "https://localhost/x.png", "", "80\n443", false],
 119              [$simpledns, "http://localhost:443/x.png", "", "80\n443", false],
 120              [$simpledns, "http://sub.example.com:8080/x.png", "", "80\n443", true],
 121              [$simpledns, "http://sub.example.com:-80/x.png", "", "80\n443", true],
 122              [$simpledns, "http://sub.example.com:aaa/x.png", "", "80\n443", true],
 123  
 124              // Test set using port filters and hosts filters.
 125              [$simpledns, "http://localhost/x.png", "127.0.0.1", "80\n443", true],
 126              [$simpledns, "http://127.0.0.1/x.png", "127.0.0.1", "80\n443", true],
 127  
 128              // Test using multiple A records.
 129              // Multiple record DNS gives two IPs for the same host, we want to make
 130              // sure that if we block one of those (doesn't matter which one)
 131              // the request is blocked.
 132              [$multiplerecorddns, "http://sub.example.com", '1.2.3.4', "", true],
 133              [$multiplerecorddns, "http://sub.example.com", '5.6.7.8', "", true],
 134  
 135              // Test when DNS resolution fails.
 136              [[], "http://example.com", "127.0.0.1", "", true],
 137  
 138              // Test ensures that the default value of getremoteaddr() 0.0.0.0 will check against the provided blocked list.
 139              [$simpledns, "http://0.0.0.0/x.png", "0.0.0.0", "", true],
 140              // Test set using IPV4 with integer format.
 141              [$simpledns, "http://2852039166/x.png", "169.254.169.254", "", true],
 142  
 143              // Test some freaky deaky Unicode domains. Should be blocked always.
 144              [$simpledns, "http://169。254。169。254/", "127.0.0.1", "", true],
 145              [$simpledns, "http://169。254。169。254/", "1.2.3.4", "", true],
 146              [$simpledns, "http://169。254。169。254/", "127.0.0.1", "80\n443", true]
 147  
 148              // Note on testing URLs using IPv6 notation:
 149              // At present, the curl_security_helper class doesn't support IPv6 url notation.
 150              // E.g.  http://[ad34::dddd]:port/resource
 151              // This is because it uses clean_param(x, PARAM_URL) as part of parsing, which won't validate urls having IPv6 notation.
 152              // The underlying IPv6 address and range support is in place, however, so if clean_param is changed in future,
 153              // please add the following test sets.
 154              // 1. ["http://[::1]/x.png", "", "", false]
 155              // 2. ["http://[::1]/x.png", "::1", "", true]
 156              // 3. ["http://[::1]/x.png", "::1/64", "", true]
 157              // 4. ["http://[fe80::dddd]/x.png", "fe80::cccc-eeee", "", true]
 158              // 5. ["http://[fe80::dddd]/x.png", "fe80::dddd/128", "", true].
 159          ];
 160      }
 161  
 162      /**
 163       * Test for \core\files\curl_security_helper->is_enabled().
 164       *
 165       * @param string $blockedhosts the list of blocked hosts.
 166       * @param string $allowedports the list of allowed ports.
 167       * @param bool $expected the expected result.
 168       * @dataProvider curl_security_settings_data_provider
 169       */
 170      public function test_curl_security_helper_is_enabled($blockedhosts, $allowedports, $expected) {
 171          $this->resetAfterTest(true);
 172          $helper = new \core\files\curl_security_helper();
 173          set_config('curlsecurityblockedhosts', $blockedhosts);
 174          set_config('curlsecurityallowedport', $allowedports);
 175          $this->assertEquals($expected, $helper->is_enabled());
 176      }
 177  
 178      /**
 179       * Data provider for test_curl_security_helper_is_enabled().
 180       *
 181       * @return array
 182       */
 183      public function curl_security_settings_data_provider() {
 184          // Format: blocked hosts, allowed ports, expected result.
 185          return [
 186              ["", "", false],
 187              ["127.0.0.1", "", true],
 188              ["localhost", "", true],
 189              ["127.0.0.0/24\n192.0.0.0/24", "", true],
 190              ["", "80\n443", true],
 191          ];
 192      }
 193  
 194      /**
 195       * Test for \core\files\curl_security_helper::host_is_blocked().
 196       *
 197       * @param string $host the host to validate.
 198       * @param string $blockedhosts the list of blocked hosts.
 199       * @param bool $expected the expected result.
 200       * @dataProvider curl_security_host_data_provider
 201       */
 202      public function test_curl_security_helper_host_is_blocked($host, $blockedhosts, $expected) {
 203          $this->resetAfterTest(true);
 204          $helper = new \core\files\curl_security_helper();
 205          set_config('curlsecurityblockedhosts', $blockedhosts);
 206          $this->assertEquals($expected, \phpunit_util::call_internal_method($helper, 'host_is_blocked', [$host],
 207                                                                            '\core\files\curl_security_helper'));
 208      }
 209  
 210      /**
 211       * Data provider for test_curl_security_helper_host_is_blocked().
 212       *
 213       * @return array
 214       */
 215      public function curl_security_host_data_provider() {
 216          return [
 217              // IPv4 hosts.
 218              ["127.0.0.1", "127.0.0.1", true],
 219              ["127.0.0.1", "127.0.0.0/24", true],
 220              ["127.0.0.1", "127.0.0.0-40", true],
 221              ["", "127.0.0.0/24", false],
 222  
 223              // IPv6 hosts.
 224              // Note: ["::", "::", true], - should match but 'address_in_subnet()' has trouble with fully collapsed IPv6 addresses.
 225              ["::1", "::1", true],
 226              ["::1", "::0-cccc", true],
 227              ["::1", "::0/64", true],
 228              ["FE80:0000:0000:0000:0000:0000:0000:0000", "fe80::/128", true],
 229              ["fe80::eeee", "fe80::ddde/64", true],
 230              ["fe80::dddd", "fe80::cccc-eeee", true],
 231              ["fe80::dddd", "fe80::ddde-eeee", false],
 232  
 233              // Domain name hosts.
 234              ["example.com", "example.com", true],
 235              ["sub.example.com", "example.com", false],
 236              ["example.com", "*.com", true],
 237              ["example.com", "*.example.com", false],
 238              ["sub.example.com", "*.example.com", true],
 239              ["sub.sub.example.com", "*.example.com", true],
 240              ["sub.example.com", "*example.com", false],
 241              ["sub.example.com", "*.example", false],
 242  
 243              // International domain name hosts.
 244              ["xn--nw2a.xn--j6w193g", "xn--nw2a.xn--j6w193g", true], // The domain 見.香港 is ace-encoded to xn--nw2a.xn--j6w193g.
 245          ];
 246      }
 247  
 248      /**
 249       * Test for \core\files\curl_security_helper->port_is_blocked().
 250       *
 251       * @param int|string $port the port to validate.
 252       * @param string $allowedports the list of allowed ports.
 253       * @param bool $expected the expected result.
 254       * @dataProvider curl_security_port_data_provider
 255       */
 256      public function test_curl_security_helper_port_is_blocked($port, $allowedports, $expected) {
 257          $this->resetAfterTest(true);
 258          $helper = new \core\files\curl_security_helper();
 259          set_config('curlsecurityallowedport', $allowedports);
 260          $this->assertEquals($expected, \phpunit_util::call_internal_method($helper, 'port_is_blocked', [$port],
 261                                                                            '\core\files\curl_security_helper'));
 262      }
 263  
 264      /**
 265       * Data provider for test_curl_security_helper_port_is_blocked().
 266       *
 267       * @return array
 268       */
 269      public function curl_security_port_data_provider() {
 270          return [
 271              ["", "80\n443", true],
 272              [" ", "80\n443", true],
 273              ["-1", "80\n443", true],
 274              [-1, "80\n443", true],
 275              ["n", "80\n443", true],
 276              [0, "80\n443", true],
 277              ["0", "80\n443", true],
 278              [8080, "80\n443", true],
 279              ["8080", "80\n443", true],
 280              ["80", "80\n443", false],
 281              [80, "80\n443", false],
 282              [443, "80\n443", false],
 283              [0, "", true], // Port 0 and below are always invalid, even when the admin hasn't set allowed entries.
 284              [-1, "", true], // Port 0 and below are always invalid, even when the admin hasn't set allowed entries.
 285              [null, "", true], // Non-string, non-int values are invalid.
 286          ];
 287      }
 288  
 289      /**
 290       * Test for \core\files\curl_security_helper::get_blocked_url_string().
 291       */
 292      public function test_curl_security_helper_get_blocked_url_string() {
 293          $helper = new \core\files\curl_security_helper();
 294          $this->assertEquals(get_string('curlsecurityurlblocked', 'admin'), $helper->get_blocked_url_string());
 295      }
 296  }