Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 * Contains a class providing functions used to check the allowed/blocked host/ports for curl.
19
 *
20
 * @package   core
21
 * @copyright 2016 Jake Dallimore
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 * @author    Jake Dallimore <jrhdallimore@gmail.com>
24
 */
25
 
26
namespace core\files;
27
use core\ip_utils;
28
 
29
defined('MOODLE_INTERNAL') || exit();
30
 
31
/**
32
 * Host and port checking for curl.
33
 *
34
 * This class provides a means to check URL/host/port against the system-level cURL security entries.
35
 * It does not provide a means to add URLs, hosts or ports to the allowed/blocked lists; this is configured manually
36
 * via the site admin section of Moodle (See: 'Site admin' > 'Security' > 'HTTP Security').
37
 *
38
 * This class is currently used by the 'curl' wrapper class in lib/filelib.php.
39
 * Depends on:
40
 *  core\ip_utils (several functions)
41
 *  moodlelib (clean_param)
42
 *
43
 * @package   core
44
 * @copyright 2016 Jake Dallimore
45
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46
 * @author    Jake Dallimore <jrhdallimore@gmail.com>
47
 */
48
class curl_security_helper extends curl_security_helper_base {
49
    /**
50
     * @var array of supported transport schemes and their respective default ports.
51
     */
52
    protected $transportschemes = [
53
        'http' => 80,
54
        'https' => 443
55
    ];
56
 
57
    /**
1441 ariadna 58
     * @var string the host of the URL being checked by the helper.
59
     */
60
    protected $host;
61
 
62
    /**
63
     * @var array IP address or addresses the URL is allowed to be requested from (passed the blocked hosts check).
64
     */
65
    protected $allowedips = [];
66
 
67
    /**
68
     * @var ?int The port the URL is allowed to be requested from (passed the allowed port check).
69
     */
70
    protected $allowedport;
71
 
72
    /**
1 efrain 73
     * Checks whether the given URL is blocked by checking its address and port number against the allow/block lists.
74
     * The behaviour of this function can be classified as strict, as it returns true for URLs which are invalid or
75
     * could not be parsed, as well as those valid URLs which were found in the blocklist.
76
     *
77
     * @param string $urlstring the URL to check.
78
     * @param int $notused There used to be an optional parameter $maxredirects for a short while here, not used any more.
79
     * @return bool true if the URL is blocked or invalid and false if the URL is not blocked.
80
     */
81
    public function url_is_blocked($urlstring, $notused = null) {
82
 
83
        if ($notused !== null) {
84
            debugging('The $maxredirects parameter of curl_security_helper::url_is_blocked() has been dropped!', DEBUG_DEVELOPER);
85
        }
86
 
87
        // If no config data is present, then all hosts/ports are allowed.
88
        if (!$this->is_enabled()) {
89
            return false;
90
        }
91
 
92
        // Try to parse the URL to get the 'host' and 'port' components.
93
        try {
94
            $url = new \moodle_url($urlstring);
95
            $parsed['scheme'] = $url->get_scheme();
96
            $parsed['host'] = $url->get_host();
97
            $parsed['port'] = $url->get_port();
98
        } catch (\moodle_exception $e) {
99
            // Moodle exception is thrown if the $urlstring is invalid. Treat as blocked.
100
            return true;
101
        }
102
 
1441 ariadna 103
        $this->host = $parsed['host'];
104
 
1 efrain 105
        // The port will be empty unless explicitly set in the $url (uncommon), so try to infer it from the supported schemes.
106
        if (!$parsed['port'] && $parsed['scheme'] && isset($this->transportschemes[$parsed['scheme']])) {
107
            $parsed['port'] = $this->transportschemes[$parsed['scheme']];
108
        }
109
 
110
        if ($parsed['port'] && $parsed['host']) {
111
            // Check the host and port against the allow/block entries.
112
            return $this->host_is_blocked($parsed['host']) || $this->port_is_blocked($parsed['port']);
113
        }
114
        return true;
115
    }
116
 
117
    /**
118
     * Returns a string message describing a blocked URL. E.g. 'This URL is blocked'.
119
     *
120
     * @return string the string error.
121
     */
122
    public function get_blocked_url_string() {
123
        return get_string('curlsecurityurlblocked', 'admin');
124
    }
125
 
126
    /**
127
     * Checks whether the host portion of a url is blocked.
128
     * The host portion may be a FQDN, IPv4 address or a IPv6 address wrapped in square brackets, as per standard URL notation.
129
     * E.g.
130
     *     images.example.com
131
     *     127.0.0.1
132
     *     [0.0.0.0.0.0.0.1]
133
     * The method logic is as follows:
134
     * 1. Check the host component against the list of IPv4/IPv6 addresses and ranges.
135
     *  - This will perform a DNS forward lookup if required.
136
     * 2. Check the host component against the list of domain names and wildcard domain names.
137
     *  - This will perform a DNS reverse lookup if required.
138
     *
139
     * The behaviour of this function can be classified as strict, as it returns true for hosts which are invalid or
140
     * could not be parsed, as well as those valid URLs which were found in the blocklist.
141
     *
142
     * @param string $host the host component of the URL to check against the blocklist.
143
     * @return bool true if the host is both valid and blocked, false otherwise.
144
     */
145
    protected function host_is_blocked($host) {
146
        if (!$this->is_enabled() || empty($host) || !is_string($host)) {
147
            return false;
148
        }
149
 
150
        // Fix for square brackets in the 'host' portion of the URL (only occurs if an IPv6 address is specified).
151
        $host = str_replace(array('[', ']'), '', $host); // RFC3986, section 3.2.2.
152
        $blockedhosts = $this->get_blocked_hosts_by_category();
153
 
154
        if (ip_utils::is_ip_address($host)) {
155
            if ($this->address_explicitly_blocked($host)) {
156
                return true;
157
            }
158
 
159
            // Only perform a reverse lookup if there is a point to it (i.e. we have rules to check against).
160
            if ($blockedhosts['domain'] || $blockedhosts['domainwildcard']) {
161
                // DNS reverse lookup - supports both IPv4 and IPv6 address formats.
162
                $hostname = gethostbyaddr($host);
163
                if ($hostname !== $host && $this->host_explicitly_blocked($hostname)) {
164
                    return true;
165
                }
166
            }
167
        } else if (ip_utils::is_domain_name($host)) {
168
            if ($this->host_explicitly_blocked($host)) {
169
                return true;
170
            }
171
 
172
            // Only perform a forward lookup if there are IP rules to check against.
173
            if ($blockedhosts['ipv4'] || $blockedhosts['ipv6']) {
174
                // DNS forward lookup - returns a list of only IPv4 addresses!
175
                $hostips = $this->get_host_list_by_name($host);
176
 
177
                // If we don't get a valid record, bail (so cURL is never called).
178
                if (!$hostips) {
179
                    return true;
180
                }
181
 
1441 ariadna 182
                // If any of the returned IPs are in the blocklist, block the request. Otherwise, temporarily record the IPs.
183
                $allowedips = [];
1 efrain 184
                foreach ($hostips as $hostip) {
185
                    if ($this->address_explicitly_blocked($hostip)) {
186
                        return true;
187
                    }
1441 ariadna 188
                    $allowedips[] = $hostip;
1 efrain 189
                }
1441 ariadna 190
 
191
                // If none of the IPs are blocked, set them on the allow list so we can enforce them on subsequent requests.
192
                $this->allowedips = $allowedips;
1 efrain 193
            }
194
        } else {
195
            // Was not something we consider to be a valid IP or domain name, block it.
196
            return true;
197
        }
198
 
199
        return false;
200
    }
201
 
202
    /**
203
     * Retrieve all hosts for a domain name.
204
     *
205
     * @param string $param
206
     * @return array An array of IPs associated with the host name.
207
     */
208
    protected function get_host_list_by_name($host) {
209
        return ($hostips = gethostbynamel($host)) ? $hostips : [];
210
    }
211
 
212
    /**
213
     * Checks whether the given port is blocked, as determined by its absence on the ports allowlist.
214
     * Ports are assumed to be blocked unless found in the allowlist.
215
     *
216
     * @param integer|string $port the port to check against the ports allowlist.
217
     * @return bool true if the port is blocked, false otherwise.
218
     */
219
    protected function port_is_blocked($port) {
220
        $portnum = intval($port);
221
        // Intentionally block port 0 and below and check the int cast was valid.
222
        if (empty($port) || (string)$portnum !== (string)$port || $port < 0) {
223
            return true;
224
        }
225
        $allowedports = $this->get_allowed_ports();
1441 ariadna 226
 
227
        $isblocked = !empty($allowedports) && !in_array($portnum, $allowedports);
228
 
229
        // If port is allowed, add it to our allow list so we can enforce it on subsequent requests.
230
        if (!$isblocked) {
231
            $this->allowedport = $portnum;
232
        }
233
 
234
        return $isblocked;
1 efrain 235
    }
236
 
237
    /**
238
     * Convenience method to check whether we have any entries in the host blocklist or ports allowlist admin settings.
239
     * If no entries are found at all, the assumption is that the blocklist is disabled entirely.
240
     *
241
     * @return bool true if one or more entries exist, false otherwise.
242
     */
243
    public function is_enabled() {
244
        return (!empty($this->get_allowed_ports()) || !empty($this->get_blocked_hosts()));
245
    }
246
 
247
    /**
248
     * Checks whether the input address is blocked by at any of the IPv4 or IPv6 address rules.
249
     *
250
     * @param string $addr the ip address to check.
251
     * @return bool true if the address is covered by an entry in the blocklist, false otherwise.
252
     */
253
    protected function address_explicitly_blocked($addr) {
254
        $blockedhosts = $this->get_blocked_hosts_by_category();
255
        $iphostsblocked = array_merge($blockedhosts['ipv4'], $blockedhosts['ipv6']);
256
        return address_in_subnet($addr, implode(',', $iphostsblocked), true);
257
    }
258
 
259
    /**
260
     * Checks whether the input hostname is blocked by any of the domain/wildcard rules.
261
     *
262
     * @param string $host the hostname to check
263
     * @return bool true if the host is covered by an entry in the blocklist, false otherwise.
264
     */
265
    protected function host_explicitly_blocked($host) {
266
        $blockedhosts = $this->get_blocked_hosts_by_category();
267
        $domainhostsblocked = array_merge($blockedhosts['domain'], $blockedhosts['domainwildcard']);
268
        return ip_utils::is_domain_in_allowed_list($host, $domainhostsblocked);
269
    }
270
 
271
    /**
272
     * Helper to get all entries from the admin setting, as an array, sorted by classification.
273
     * Classifications include 'ipv4', 'ipv6', 'domain', 'domainwildcard'.
274
     *
275
     * @return array of host/domain/ip entries from the 'curlsecurityblockedhosts' config.
276
     */
277
    protected function get_blocked_hosts_by_category() {
278
        // For each of the admin setting entries, check and place in the correct section of the config array.
279
        $config = ['ipv6' => [], 'ipv4' => [], 'domain' => [], 'domainwildcard' => []];
280
        $entries = $this->get_blocked_hosts();
281
        foreach ($entries as $entry) {
282
            if (ip_utils::is_ipv6_address($entry) || ip_utils::is_ipv6_range($entry)) {
283
                $config['ipv6'][] = $entry;
284
            } else if (ip_utils::is_ipv4_address($entry) || ip_utils::is_ipv4_range($entry)) {
285
                $config['ipv4'][] = $entry;
286
            } else if (ip_utils::is_domain_name($entry)) {
287
                $config['domain'][] = $entry;
288
            } else if (ip_utils::is_domain_matching_pattern($entry)) {
289
                $config['domainwildcard'][] = $entry;
290
            }
291
        }
292
        return $config;
293
    }
294
 
295
    /**
296
     * Helper that returns the allowed ports, as defined in the 'curlsecurityallowedport' setting.
297
     *
298
     * @return array the array of allowed ports.
299
     */
300
    protected function get_allowed_ports() {
301
        global $CFG;
302
        if (!isset($CFG->curlsecurityallowedport)) {
303
            return [];
304
        }
305
        return array_filter(array_map('trim', explode("\n", $CFG->curlsecurityallowedport)), function($entry) {
306
            return !empty($entry);
307
        });
308
    }
309
 
310
    /**
311
     * Helper that returns the blocked hosts, as defined in the 'curlsecurityblockedhosts' setting.
312
     *
313
     * @return array the array of blocked host entries.
314
     */
315
    protected function get_blocked_hosts() {
316
        global $CFG;
317
        if (!isset($CFG->curlsecurityblockedhosts)) {
318
            return [];
319
        }
320
        return array_filter(array_map('trim', explode("\n", $CFG->curlsecurityblockedhosts)), function($entry) {
321
            return !empty($entry);
322
        });
323
    }
1441 ariadna 324
 
325
    /**
326
     * Helper that returns host, IP and port information for the URL that has passed the blocked hosts/allowed ports checks.
327
     *
328
     * This data is in a format compatible with CURLOPT_RESOLVE, so it can be passed directly into that option.
329
     * Doing so will prevent cURL re-fetching the info from DNS, preventing subsequent requests to the remote host from
330
     * modifying the IP/port to ones that haven't been validated.
331
     *
332
     * @return array of strings in the format hostname:port:ip_address.
333
     * @throws \coding_exception
334
     */
335
    public function get_resolve_info(): array {
336
        if (empty($this->host || empty($this->allowedips) || empty($this->allowedport))) {
337
            $exception = 'In the curl_security_helper class, url_is_blocked() must be called before get_resolve_info() is called.';
338
            throw new \core\exception\coding_exception($exception);
339
        }
340
 
341
        return array_map(fn($ip) => "$this->host:$this->allowedport:$ip", $this->allowedips);
342
    }
1 efrain 343
}