1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace SimpleSAML\Utils; |
||
6 | |||
7 | use SimpleSAML\Configuration; |
||
8 | use SimpleSAML\Error; |
||
9 | use SimpleSAML\Logger; |
||
10 | use SimpleSAML\Module; |
||
11 | use SimpleSAML\Session; |
||
12 | use SimpleSAML\XHTML\Template; |
||
13 | |||
14 | /** |
||
15 | * HTTP-related utility methods. |
||
16 | * |
||
17 | * @package SimpleSAMLphp |
||
18 | */ |
||
19 | class HTTP |
||
20 | { |
||
21 | /** |
||
22 | * Obtain a URL where we can redirect to securely post a form with the given data to a specific destination. |
||
23 | * |
||
24 | * @param string $destination The destination URL. |
||
25 | * @param array $data An associative array containing the data to be posted to $destination. |
||
26 | * |
||
27 | * @throws Error\Exception If the current session is transient. |
||
28 | * @return string A URL which allows to securely post a form to $destination. |
||
29 | * |
||
30 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
31 | */ |
||
32 | private static function getSecurePOSTRedirectURL(string $destination, array $data): string |
||
33 | { |
||
34 | $session = Session::getSessionFromRequest(); |
||
35 | $id = self::savePOSTData($session, $destination, $data); |
||
36 | |||
37 | if ($session->isTransient()) { |
||
38 | // this is a transient session, it is pointless to continue |
||
39 | throw new Error\Exception('Cannot save POST data to a transient session.'); |
||
40 | } |
||
41 | |||
42 | /** @var string $session_id */ |
||
43 | $session_id = $session->getSessionId(); |
||
44 | |||
45 | // encrypt the session ID and the random ID |
||
46 | $info = base64_encode(Crypto::aesEncrypt($session_id . ':' . $id)); |
||
47 | |||
48 | $url = Module::getModuleURL('core/postredirect.php', ['RedirInfo' => $info]); |
||
49 | return preg_replace('#^https:#', 'http:', $url); |
||
50 | } |
||
51 | |||
52 | |||
53 | /** |
||
54 | * Retrieve Host value from $_SERVER environment variables. |
||
55 | * |
||
56 | * @return string The current host name, including the port if needed. It will use localhost when unable to |
||
57 | * determine the current host. |
||
58 | * |
||
59 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
60 | */ |
||
61 | private static function getServerHost(): string |
||
62 | { |
||
63 | if (array_key_exists('HTTP_HOST', $_SERVER)) { |
||
64 | $current = $_SERVER['HTTP_HOST']; |
||
65 | } elseif (array_key_exists('SERVER_NAME', $_SERVER)) { |
||
66 | $current = $_SERVER['SERVER_NAME']; |
||
67 | } else { |
||
68 | // almost certainly not what you want, but... |
||
69 | $current = 'localhost'; |
||
70 | } |
||
71 | |||
72 | if (strstr($current, ":")) { |
||
73 | $decomposed = explode(":", $current); |
||
74 | $port = array_pop($decomposed); |
||
75 | if (!is_numeric($port)) { |
||
76 | array_push($decomposed, $port); |
||
77 | } |
||
78 | $current = implode(":", $decomposed); |
||
79 | } |
||
80 | return $current; |
||
81 | } |
||
82 | |||
83 | |||
84 | /** |
||
85 | * Retrieve HTTPS status from $_SERVER environment variables. |
||
86 | * |
||
87 | * @return boolean True if the request was performed through HTTPS, false otherwise. |
||
88 | * |
||
89 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
90 | */ |
||
91 | public static function getServerHTTPS() |
||
92 | { |
||
93 | if (!array_key_exists('HTTPS', $_SERVER)) { |
||
94 | // not an https-request |
||
95 | return false; |
||
96 | } |
||
97 | |||
98 | if ($_SERVER['HTTPS'] === 'off') { |
||
99 | // IIS with HTTPS off |
||
100 | return false; |
||
101 | } |
||
102 | |||
103 | // otherwise, HTTPS will be non-empty |
||
104 | return !empty($_SERVER['HTTPS']); |
||
105 | } |
||
106 | |||
107 | |||
108 | /** |
||
109 | * Retrieve the port number from $_SERVER environment variables. |
||
110 | * |
||
111 | * @return string The port number prepended by a colon, if it is different than the default port for the protocol |
||
112 | * (80 for HTTP, 443 for HTTPS), or an empty string otherwise. |
||
113 | * |
||
114 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
115 | */ |
||
116 | public static function getServerPort() |
||
117 | { |
||
118 | $default_port = self::getServerHTTPS() ? '443' : '80'; |
||
119 | $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : $default_port; |
||
120 | |||
121 | // Take care of edge-case where SERVER_PORT is an integer |
||
122 | $port = strval($port); |
||
123 | |||
124 | if ($port !== $default_port) { |
||
125 | return ':' . $port; |
||
126 | } |
||
127 | return ''; |
||
128 | } |
||
129 | |||
130 | |||
131 | /** |
||
132 | * Verify that a given URL is valid. |
||
133 | * |
||
134 | * @param string $url The URL we want to verify. |
||
135 | * |
||
136 | * @return boolean True if the given URL is valid, false otherwise. |
||
137 | */ |
||
138 | public static function isValidURL($url) |
||
139 | { |
||
140 | $url = filter_var($url, FILTER_VALIDATE_URL); |
||
141 | if ($url === false) { |
||
142 | return false; |
||
143 | } |
||
144 | $scheme = parse_url($url, PHP_URL_SCHEME); |
||
145 | if (is_string($scheme) && in_array(strtolower($scheme), ['http', 'https'], true)) { |
||
146 | return true; |
||
147 | } |
||
148 | return false; |
||
149 | } |
||
150 | |||
151 | |||
152 | /** |
||
153 | * This function redirects the user to the specified address. |
||
154 | * |
||
155 | * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the |
||
156 | * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used. |
||
157 | * |
||
158 | * The function will also generate a simple web page with a clickable link to the target page. |
||
159 | * |
||
160 | * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a |
||
161 | * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the |
||
162 | * absolute URL to the root of the website. |
||
163 | * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The |
||
164 | * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both |
||
165 | * the name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just |
||
166 | * the name, without a value. |
||
167 | * |
||
168 | * @return void This function never returns. |
||
169 | * @throws \InvalidArgumentException If $url is not a string or is empty, or $parameters is not an array. |
||
170 | * @throws \SimpleSAML\Error\Exception If $url is not a valid HTTP URL. |
||
171 | * |
||
172 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
173 | * @author Mads Freek Petersen |
||
174 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
175 | */ |
||
176 | private static function redirect(string $url, array $parameters = []) |
||
177 | { |
||
178 | if (empty($url)) { |
||
179 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
180 | } |
||
181 | if (!self::isValidURL($url)) { |
||
182 | throw new Error\Exception('Invalid destination URL.'); |
||
183 | } |
||
184 | |||
185 | if (!empty($parameters)) { |
||
186 | $url = self::addURLParameters($url, $parameters); |
||
187 | } |
||
188 | |||
189 | /* Set the HTTP result code. This is either 303 See Other or |
||
190 | * 302 Found. HTTP 303 See Other is sent if the HTTP version |
||
191 | * is HTTP/1.1 and the request type was a POST request. |
||
192 | */ |
||
193 | if ( |
||
194 | $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1' |
||
195 | && $_SERVER['REQUEST_METHOD'] === 'POST' |
||
196 | ) { |
||
197 | $code = 303; |
||
198 | } else { |
||
199 | $code = 302; |
||
200 | } |
||
201 | |||
202 | if (strlen($url) > 2048) { |
||
203 | Logger::warning('Redirecting to a URL longer than 2048 bytes.'); |
||
204 | } |
||
205 | |||
206 | if (!headers_sent()) { |
||
207 | // set the location header |
||
208 | header('Location: ' . $url, true, $code); |
||
0 ignored issues
–
show
|
|||
209 | |||
210 | // disable caching of this response |
||
211 | header('Pragma: no-cache'); |
||
212 | header('Cache-Control: no-cache, no-store, must-revalidate'); |
||
213 | } |
||
214 | |||
215 | // show a minimal web page with a clickable link to the URL |
||
216 | echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; |
||
217 | echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'; |
||
218 | echo ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n"; |
||
219 | echo '<html xmlns="http://www.w3.org/1999/xhtml">' . "\n"; |
||
220 | echo " <head>\n"; |
||
221 | echo ' <meta http-equiv="content-type" content="text/html; charset=utf-8">' . "\n"; |
||
222 | echo ' <meta http-equiv="refresh" content="0;URL=\'' . htmlspecialchars($url) . '\'">' . "\n"; |
||
223 | echo " <title>Redirect</title>\n"; |
||
224 | echo " </head>\n"; |
||
225 | echo " <body>\n"; |
||
226 | echo " <h1>Redirect</h1>\n"; |
||
227 | echo ' <p>You were redirected to: <a id="redirlink" href="' . htmlspecialchars($url) . '">'; |
||
228 | echo htmlspecialchars($url) . "</a>\n"; |
||
229 | echo ' <script type="text/javascript">document.getElementById("redirlink").focus();</script>' . "\n"; |
||
230 | echo " </p>\n"; |
||
231 | echo " </body>\n"; |
||
232 | echo '</html>'; |
||
233 | |||
234 | // end script execution |
||
235 | exit; |
||
236 | } |
||
237 | |||
238 | |||
239 | /** |
||
240 | * Save the given HTTP POST data and the destination where it should be posted to a given session. |
||
241 | * |
||
242 | * @param \SimpleSAML\Session $session The session where to temporarily store the data. |
||
243 | * @param string $destination The destination URL where the form should be posted. |
||
244 | * @param array $data An associative array with the data to be posted to $destination. |
||
245 | * |
||
246 | * @return string A random identifier that can be used to retrieve the data from the current session. |
||
247 | * |
||
248 | * @author Andjelko Horvat |
||
249 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
250 | */ |
||
251 | private static function savePOSTData(Session $session, string $destination, array $data): string |
||
252 | { |
||
253 | // generate a random ID to avoid replay attacks |
||
254 | $id = Random::generateID(); |
||
255 | $postData = [ |
||
256 | 'post' => $data, |
||
257 | 'url' => $destination, |
||
258 | ]; |
||
259 | |||
260 | // save the post data to the session, tied to the random ID |
||
261 | $session->setData('core_postdatalink', $id, $postData); |
||
262 | |||
263 | return $id; |
||
264 | } |
||
265 | |||
266 | |||
267 | /** |
||
268 | * Add one or more query parameters to the given URL. |
||
269 | * |
||
270 | * @param string $url The URL the query parameters should be added to. |
||
271 | * @param array $parameters The query parameters which should be added to the url. This should be an associative |
||
272 | * array. |
||
273 | * |
||
274 | * @return string The URL with the new query parameters. |
||
275 | * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. |
||
276 | * |
||
277 | * @author Andreas Solberg, UNINETT AS <[email protected]> |
||
278 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
279 | */ |
||
280 | public static function addURLParameters($url, $parameters) |
||
281 | { |
||
282 | if (!is_string($url) || !is_array($parameters)) { |
||
283 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
284 | } |
||
285 | |||
286 | $queryStart = strpos($url, '?'); |
||
287 | if ($queryStart === false) { |
||
288 | $oldQuery = []; |
||
289 | $url .= '?'; |
||
290 | } else { |
||
291 | /** @var string|false $oldQuery */ |
||
292 | $oldQuery = substr($url, $queryStart + 1); |
||
293 | if ($oldQuery === false) { |
||
294 | $oldQuery = []; |
||
295 | } else { |
||
296 | $oldQuery = self::parseQueryString($oldQuery); |
||
297 | } |
||
298 | $url = substr($url, 0, $queryStart + 1); |
||
299 | } |
||
300 | |||
301 | /** @var array $oldQuery */ |
||
302 | $query = array_merge($oldQuery, $parameters); |
||
303 | $url .= http_build_query($query, '', '&'); |
||
304 | |||
305 | return $url; |
||
306 | } |
||
307 | |||
308 | |||
309 | /** |
||
310 | * Check for session cookie, and show missing-cookie page if it is missing. |
||
311 | * |
||
312 | * @param string|null $retryURL The URL the user should access to retry the operation. Defaults to null. |
||
313 | * |
||
314 | * @return void If there is a session cookie, nothing will be returned. Otherwise, the user will be redirected to a |
||
315 | * page telling about the missing cookie. |
||
316 | * @throws \InvalidArgumentException If $retryURL is neither a string nor null. |
||
317 | * |
||
318 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
319 | */ |
||
320 | public static function checkSessionCookie($retryURL = null) |
||
321 | { |
||
322 | if (!is_null($retryURL) && !is_string($retryURL)) { |
||
323 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
324 | } |
||
325 | |||
326 | $session = Session::getSessionFromRequest(); |
||
327 | if ($session->hasSessionCookie()) { |
||
328 | return; |
||
329 | } |
||
330 | |||
331 | // we didn't have a session cookie. Redirect to the no-cookie page |
||
332 | |||
333 | $url = Module::getModuleURL('core/no_cookie.php'); |
||
334 | if ($retryURL !== null) { |
||
335 | $url = self::addURLParameters($url, ['retryURL' => $retryURL]); |
||
336 | } |
||
337 | self::redirectTrustedURL($url); |
||
338 | } |
||
339 | |||
340 | |||
341 | /** |
||
342 | * Check if a URL is valid and is in our list of allowed URLs. |
||
343 | * |
||
344 | * @param string $url The URL to check. |
||
345 | * @param array $trustedSites An optional white list of domains. If none specified, the 'trusted.url.domains' |
||
346 | * configuration directive will be used. |
||
347 | * |
||
348 | * @return string The normalized URL itself if it is allowed. An empty string if the $url parameter is empty as |
||
349 | * defined by the empty() function. |
||
350 | * @throws \InvalidArgumentException If the URL is malformed. |
||
351 | * @throws Error\Exception If the URL is not allowed by configuration. |
||
352 | * |
||
353 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
354 | */ |
||
355 | public static function checkURLAllowed($url, array $trustedSites = null) |
||
356 | { |
||
357 | if (empty($url)) { |
||
358 | return ''; |
||
359 | } |
||
360 | $url = self::normalizeURL($url); |
||
361 | |||
362 | if (!self::isValidURL($url)) { |
||
363 | throw new Error\Exception('Invalid URL: ' . $url); |
||
364 | } |
||
365 | |||
366 | // get the white list of domains |
||
367 | if ($trustedSites === null) { |
||
368 | $trustedSites = Configuration::getInstance()->getValue('trusted.url.domains', []); |
||
369 | } |
||
370 | |||
371 | // validates the URL's host is among those allowed |
||
372 | if (is_array($trustedSites)) { |
||
373 | $components = parse_url($url); |
||
374 | $hostname = $components['host']; |
||
375 | |||
376 | // check for userinfo |
||
377 | if ( |
||
378 | (isset($components['user']) |
||
379 | && strpos($components['user'], '\\') !== false) |
||
380 | || (isset($components['pass']) |
||
381 | && strpos($components['pass'], '\\') !== false) |
||
382 | ) { |
||
383 | throw new Error\Exception('Invalid URL: ' . $url); |
||
384 | } |
||
385 | |||
386 | // allow URLs with standard ports specified (non-standard ports must then be allowed explicitly) |
||
387 | if ( |
||
388 | isset($components['port']) |
||
389 | && (($components['scheme'] === 'http' |
||
390 | && $components['port'] !== 80) |
||
391 | || ($components['scheme'] === 'https' |
||
392 | && $components['port'] !== 443)) |
||
393 | ) { |
||
394 | $hostname = $hostname . ':' . $components['port']; |
||
395 | } |
||
396 | |||
397 | $self_host = self::getSelfHostWithNonStandardPort(); |
||
398 | |||
399 | $trustedRegex = Configuration::getInstance()->getValue('trusted.url.regex', false); |
||
400 | |||
401 | $trusted = false; |
||
402 | if ($trustedRegex) { |
||
403 | // add self host to the white list |
||
404 | $trustedSites[] = preg_quote($self_host); |
||
405 | foreach ($trustedSites as $regex) { |
||
406 | // Add start and end delimiters. |
||
407 | $regex = "@^{$regex}$@"; |
||
408 | if (preg_match($regex, $hostname)) { |
||
409 | $trusted = true; |
||
410 | break; |
||
411 | } |
||
412 | } |
||
413 | } else { |
||
414 | // add self host to the white list |
||
415 | $trustedSites[] = $self_host; |
||
416 | $trusted = in_array($hostname, $trustedSites, true); |
||
417 | } |
||
418 | |||
419 | // throw exception due to redirection to untrusted site |
||
420 | if (!$trusted) { |
||
421 | throw new Error\Exception('URL not allowed: ' . $url); |
||
422 | } |
||
423 | } |
||
424 | return $url; |
||
425 | } |
||
426 | |||
427 | |||
428 | /** |
||
429 | * Helper function to retrieve a file or URL with proxy support, also |
||
430 | * supporting proxy basic authorization.. |
||
431 | * |
||
432 | * An exception will be thrown if we are unable to retrieve the data. |
||
433 | * |
||
434 | * @param string $url The path or URL we should fetch. |
||
435 | * @param array $context Extra context options. This parameter is optional. |
||
436 | * @param boolean $getHeaders Whether to also return response headers. Optional. |
||
437 | * |
||
438 | * @return string|array An array if $getHeaders is set, containing the data and the headers respectively; string |
||
439 | * otherwise. |
||
440 | * @throws \InvalidArgumentException If the input parameters are invalid. |
||
441 | * @throws Error\Exception If the file or URL cannot be retrieved. |
||
442 | * |
||
443 | * @author Andjelko Horvat |
||
444 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
445 | * @author Marco Ferrante, University of Genova <[email protected]> |
||
446 | */ |
||
447 | public static function fetch($url, $context = [], $getHeaders = false) |
||
448 | { |
||
449 | if (!is_string($url)) { |
||
450 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
451 | } |
||
452 | |||
453 | $config = Configuration::getInstance(); |
||
454 | |||
455 | $proxy = $config->getString('proxy', null); |
||
456 | if ($proxy !== null) { |
||
457 | if (!isset($context['http']['proxy'])) { |
||
458 | $context['http']['proxy'] = $proxy; |
||
459 | } |
||
460 | $proxy_auth = $config->getString('proxy.auth', false); |
||
461 | if ($proxy_auth !== false) { |
||
462 | $context['http']['header'] = "Proxy-Authorization: Basic " . base64_encode($proxy_auth); |
||
463 | } |
||
464 | if (!isset($context['http']['request_fulluri'])) { |
||
465 | $context['http']['request_fulluri'] = true; |
||
466 | } |
||
467 | /* |
||
468 | * If the remote endpoint over HTTPS uses the SNI extension (Server Name Indication RFC 4366), the proxy |
||
469 | * could introduce a mismatch between the names in the Host: HTTP header and the SNI_server_name in TLS |
||
470 | * negotiation (thanks to Cristiano Valli @ GARR-IDEM to have pointed this problem). |
||
471 | * See: https://bugs.php.net/bug.php?id=63519 |
||
472 | * These controls will force the same value for both fields. |
||
473 | * Marco Ferrante ([email protected]), Nov 2012 |
||
474 | */ |
||
475 | if ( |
||
476 | preg_match('#^https#i', $url) |
||
477 | && defined('OPENSSL_TLSEXT_SERVER_NAME') |
||
478 | && OPENSSL_TLSEXT_SERVER_NAME |
||
479 | ) { |
||
480 | // extract the hostname |
||
481 | $hostname = parse_url($url, PHP_URL_HOST); |
||
482 | if (!empty($hostname)) { |
||
483 | $context['ssl'] = [ |
||
484 | 'SNI_server_name' => $hostname, |
||
485 | 'SNI_enabled' => true, |
||
486 | ]; |
||
487 | } else { |
||
488 | Logger::warning('Invalid URL format or local URL used through a proxy'); |
||
489 | } |
||
490 | } |
||
491 | } |
||
492 | |||
493 | $context = stream_context_create($context); |
||
494 | $data = @file_get_contents($url, false, $context); |
||
495 | if ($data === false) { |
||
496 | $error = error_get_last(); |
||
497 | throw new Error\Exception('Error fetching ' . var_export($url, true) . ':' . |
||
498 | (is_array($error) ? $error['message'] : 'no error available')); |
||
499 | } |
||
500 | |||
501 | // data and headers |
||
502 | if ($getHeaders) { |
||
503 | /** |
||
504 | * @psalm-suppress UndefinedVariable Remove when Psalm >= 3.0.17 |
||
505 | */ |
||
506 | if (!empty($http_response_header)) { |
||
507 | $headers = []; |
||
508 | /** |
||
509 | * @psalm-suppress UndefinedVariable Remove when Psalm >= 3.0.17 |
||
510 | */ |
||
511 | foreach ($http_response_header as $h) { |
||
512 | if (preg_match('@^HTTP/1\.[01]\s+\d{3}\s+@', $h)) { |
||
513 | $headers = []; // reset |
||
514 | $headers[0] = $h; |
||
515 | continue; |
||
516 | } |
||
517 | $bits = explode(':', $h, 2); |
||
518 | if (count($bits) === 2) { |
||
519 | $headers[strtolower($bits[0])] = trim($bits[1]); |
||
520 | } |
||
521 | } |
||
522 | } else { |
||
523 | // no HTTP headers, probably a different protocol, e.g. file |
||
524 | $headers = null; |
||
525 | } |
||
526 | return [$data, $headers]; |
||
527 | } |
||
528 | |||
529 | return $data; |
||
530 | } |
||
531 | |||
532 | |||
533 | /** |
||
534 | * This function parses the Accept-Language HTTP header and returns an associative array with each language and the |
||
535 | * score for that language. If a language includes a region, then the result will include both the language with |
||
536 | * the region and the language without the region. |
||
537 | * |
||
538 | * The returned array will be in the same order as the input. |
||
539 | * |
||
540 | * @return array An associative array with each language and the score for that language. |
||
541 | * |
||
542 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
543 | */ |
||
544 | public static function getAcceptLanguage() |
||
545 | { |
||
546 | if (!array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) { |
||
547 | // no Accept-Language header, return an empty set |
||
548 | return []; |
||
549 | } |
||
550 | |||
551 | $languages = explode(',', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE'])); |
||
552 | |||
553 | $ret = []; |
||
554 | |||
555 | foreach ($languages as $l) { |
||
556 | $opts = explode(';', $l); |
||
557 | |||
558 | $l = trim(array_shift($opts)); // the language is the first element |
||
559 | |||
560 | $q = 1.0; |
||
561 | |||
562 | // iterate over all options, and check for the quality option |
||
563 | foreach ($opts as $o) { |
||
564 | $o = explode('=', $o); |
||
565 | if (count($o) < 2) { |
||
566 | // skip option with no value |
||
567 | continue; |
||
568 | } |
||
569 | |||
570 | $name = trim($o[0]); |
||
571 | $value = trim($o[1]); |
||
572 | |||
573 | if ($name === 'q') { |
||
574 | $q = (float) $value; |
||
575 | } |
||
576 | } |
||
577 | |||
578 | // remove the old key to ensure that the element is added to the end |
||
579 | unset($ret[$l]); |
||
580 | |||
581 | // set the quality in the result |
||
582 | $ret[$l] = $q; |
||
583 | |||
584 | if (strpos($l, '-')) { |
||
585 | // the language includes a region part |
||
586 | |||
587 | // extract the language without the region |
||
588 | $l = explode('-', $l); |
||
589 | $l = $l[0]; |
||
590 | |||
591 | // add this language to the result (unless it is defined already) |
||
592 | if (!array_key_exists($l, $ret)) { |
||
593 | $ret[$l] = $q; |
||
594 | } |
||
595 | } |
||
596 | } |
||
597 | return $ret; |
||
598 | } |
||
599 | |||
600 | |||
601 | /** |
||
602 | * Try to guess the base SimpleSAMLphp path from the current request. |
||
603 | * |
||
604 | * This method offers just a guess, so don't rely on it. |
||
605 | * |
||
606 | * @return string The guessed base path that should correspond to the root installation of SimpleSAMLphp. |
||
607 | */ |
||
608 | public static function guessBasePath() |
||
609 | { |
||
610 | if (!array_key_exists('REQUEST_URI', $_SERVER) || !array_key_exists('SCRIPT_FILENAME', $_SERVER)) { |
||
611 | return '/'; |
||
612 | } |
||
613 | // get the name of the current script |
||
614 | $path = explode('/', $_SERVER['SCRIPT_FILENAME']); |
||
615 | $script = array_pop($path); |
||
616 | |||
617 | // get the portion of the URI up to the script, i.e.: /simplesaml/some/directory/script.php |
||
618 | if (!preg_match('#^/(?:[^/]+/)*' . $script . '#', $_SERVER['REQUEST_URI'], $matches)) { |
||
619 | return '/'; |
||
620 | } |
||
621 | $uri_s = explode('/', $matches[0]); |
||
622 | $file_s = explode('/', $_SERVER['SCRIPT_FILENAME']); |
||
623 | |||
624 | // compare both arrays from the end, popping elements matching out of them |
||
625 | while ($uri_s[count($uri_s) - 1] === $file_s[count($file_s) - 1]) { |
||
626 | array_pop($uri_s); |
||
627 | array_pop($file_s); |
||
628 | } |
||
629 | // we are now left with the minimum part of the URI that does not match anything in the file system, use it |
||
630 | return join('/', $uri_s) . '/'; |
||
631 | } |
||
632 | |||
633 | |||
634 | /** |
||
635 | * Retrieve the base URL of the SimpleSAMLphp installation. The URL will always end with a '/'. For example: |
||
636 | * https://idp.example.org/simplesaml/ |
||
637 | * |
||
638 | * @return string The absolute base URL for the SimpleSAMLphp installation. |
||
639 | * @throws \SimpleSAML\Error\CriticalConfigurationError If 'baseurlpath' has an invalid format. |
||
640 | * |
||
641 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
642 | */ |
||
643 | public static function getBaseURL() |
||
644 | { |
||
645 | $globalConfig = Configuration::getInstance(); |
||
646 | $baseURL = $globalConfig->getString('baseurlpath', 'simplesaml/'); |
||
647 | |||
648 | if (preg_match('#^https?://.*/?$#D', $baseURL, $matches)) { |
||
649 | // full URL in baseurlpath, override local server values |
||
650 | return rtrim($baseURL, '/') . '/'; |
||
651 | } elseif ( |
||
652 | (preg_match('#^/?([^/]?.*/)$#D', $baseURL, $matches)) |
||
653 | || (preg_match('#^\*(.*)/$#D', $baseURL, $matches)) |
||
654 | || ($baseURL === '') |
||
655 | ) { |
||
656 | // get server values |
||
657 | $protocol = 'http'; |
||
658 | $protocol .= (self::getServerHTTPS()) ? 's' : ''; |
||
659 | $protocol .= '://'; |
||
660 | |||
661 | $hostname = self::getServerHost(); |
||
662 | $port = self::getServerPort(); |
||
663 | $path = $globalConfig->getBasePath(); |
||
664 | |||
665 | return $protocol . $hostname . $port . $path; |
||
666 | } else { |
||
667 | /* |
||
668 | * Invalid 'baseurlpath'. We cannot recover from this, so throw a critical exception and try to be graceful |
||
669 | * with the configuration. Use a guessed base path instead of the one provided. |
||
670 | */ |
||
671 | $c = $globalConfig->toArray(); |
||
672 | $c['baseurlpath'] = self::guessBasePath(); |
||
673 | throw new Error\CriticalConfigurationError( |
||
674 | 'Invalid value for \'baseurlpath\' in config.php. Valid format is in the form: ' . |
||
675 | '[(http|https)://(hostname|fqdn)[:port]]/[path/to/simplesaml/]. It must end with a \'/\'.', |
||
676 | null, |
||
677 | $c |
||
678 | ); |
||
679 | } |
||
680 | } |
||
681 | |||
682 | |||
683 | /** |
||
684 | * Retrieve the first element of the URL path. |
||
685 | * |
||
686 | * @param boolean $leadingSlash Whether to add a leading slash to the element or not. Defaults to true. |
||
687 | * |
||
688 | * @return string The first element of the URL path, with an optional, leading slash. |
||
689 | * |
||
690 | * @author Andreas Solberg, UNINETT AS <[email protected]> |
||
691 | */ |
||
692 | public static function getFirstPathElement($leadingSlash = true) |
||
693 | { |
||
694 | if (preg_match('|^/(.*?)/|', $_SERVER['SCRIPT_NAME'], $matches)) { |
||
695 | return ($leadingSlash ? '/' : '') . $matches[1]; |
||
696 | } |
||
697 | return ''; |
||
698 | } |
||
699 | |||
700 | |||
701 | /** |
||
702 | * Create a link which will POST data. |
||
703 | * |
||
704 | * @param string $destination The destination URL. |
||
705 | * @param array $data The name-value pairs which will be posted to the destination. |
||
706 | * |
||
707 | * @return string A URL which can be accessed to post the data. |
||
708 | * @throws \InvalidArgumentException If $destination is not a string or $data is not an array. |
||
709 | * |
||
710 | * @author Andjelko Horvat |
||
711 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
712 | */ |
||
713 | public static function getPOSTRedirectURL($destination, $data) |
||
714 | { |
||
715 | if (!is_string($destination) || !is_array($data)) { |
||
716 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
717 | } |
||
718 | |||
719 | $config = Configuration::getInstance(); |
||
720 | $allowed = $config->getBoolean('enable.http_post', false); |
||
721 | |||
722 | if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) { |
||
723 | // we need to post the data to HTTP |
||
724 | $url = self::getSecurePOSTRedirectURL($destination, $data); |
||
725 | } else { |
||
726 | // post the data directly |
||
727 | $session = Session::getSessionFromRequest(); |
||
728 | $id = self::savePOSTData($session, $destination, $data); |
||
729 | $url = Module::getModuleURL('core/postredirect.php', ['RedirId' => $id]); |
||
730 | } |
||
731 | |||
732 | return $url; |
||
733 | } |
||
734 | |||
735 | |||
736 | /** |
||
737 | * Retrieve our own host. |
||
738 | * |
||
739 | * E.g. www.example.com |
||
740 | * |
||
741 | * @return string The current host. |
||
742 | * |
||
743 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
744 | */ |
||
745 | public static function getSelfHost() |
||
746 | { |
||
747 | $decomposed = explode(':', self::getSelfHostWithNonStandardPort()); |
||
748 | return array_shift($decomposed); |
||
749 | } |
||
750 | |||
751 | /** |
||
752 | * Retrieve our own host, including the port in case the it is not standard for the protocol in use. That is port |
||
753 | * 80 for HTTP and port 443 for HTTPS. |
||
754 | * |
||
755 | * E.g. www.example.com:8080 |
||
756 | * |
||
757 | * @return string The current host, followed by a colon and the port number, in case the port is not standard for |
||
758 | * the protocol. |
||
759 | * |
||
760 | * @author Andreas Solberg, UNINETT AS <[email protected]> |
||
761 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
762 | */ |
||
763 | public static function getSelfHostWithNonStandardPort() |
||
764 | { |
||
765 | $url = self::getBaseURL(); |
||
766 | |||
767 | /** @var int $colon getBaseURL() will allways return a valid URL */ |
||
768 | $colon = strpos($url, '://'); |
||
769 | $start = $colon + 3; |
||
770 | $length = strcspn($url, '/', $start); |
||
771 | |||
772 | return substr($url, $start, $length); |
||
773 | } |
||
774 | |||
775 | |||
776 | /** |
||
777 | * Retrieve our own host together with the URL path. Please note this function will return the base URL for the |
||
778 | * current SP, as defined in the global configuration. |
||
779 | * |
||
780 | * @return string The current host (with non-default ports included) plus the URL path. |
||
781 | * |
||
782 | * @author Andreas Solberg, UNINETT AS <[email protected]> |
||
783 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
784 | */ |
||
785 | public static function getSelfHostWithPath() |
||
786 | { |
||
787 | $baseurl = explode("/", self::getBaseURL()); |
||
788 | $elements = array_slice($baseurl, 3 - count($baseurl), count($baseurl) - 4); |
||
789 | $path = implode("/", $elements); |
||
790 | return self::getSelfHostWithNonStandardPort() . "/" . $path; |
||
791 | } |
||
792 | |||
793 | |||
794 | /** |
||
795 | * Retrieve the current URL using the base URL in the configuration, if possible. |
||
796 | * |
||
797 | * This method will try to see if the current script is part of SimpleSAMLphp. In that case, it will use the |
||
798 | * 'baseurlpath' configuration option to rebuild the current URL based on that. If the current script is NOT |
||
799 | * part of SimpleSAMLphp, it will just return the current URL. |
||
800 | * |
||
801 | * Note that this method does NOT make use of the HTTP X-Forwarded-* set of headers. |
||
802 | * |
||
803 | * @return string The current URL, including query parameters. |
||
804 | * |
||
805 | * @author Andreas Solberg, UNINETT AS <[email protected]> |
||
806 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
807 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
808 | */ |
||
809 | public static function getSelfURL() |
||
810 | { |
||
811 | $cfg = Configuration::getInstance(); |
||
812 | $baseDir = $cfg->getBaseDir(); |
||
813 | $cur_path = realpath($_SERVER['SCRIPT_FILENAME']); |
||
814 | // make sure we got a string from realpath() |
||
815 | $cur_path = is_string($cur_path) ? $cur_path : ''; |
||
816 | // find the path to the current script relative to the www/ directory of SimpleSAMLphp |
||
817 | $rel_path = str_replace($baseDir . 'www' . DIRECTORY_SEPARATOR, '', $cur_path); |
||
818 | // convert that relative path to an HTTP query |
||
819 | $url_path = str_replace(DIRECTORY_SEPARATOR, '/', $rel_path); |
||
820 | // find where the relative path starts in the current request URI |
||
821 | $uri_pos = (!empty($url_path)) ? strpos($_SERVER['REQUEST_URI'] ?? '', $url_path) : false; |
||
822 | |||
823 | if ($cur_path == $rel_path || $uri_pos === false) { |
||
824 | /* |
||
825 | * We were accessed from an external script. This can happen in the following cases: |
||
826 | * |
||
827 | * - $_SERVER['SCRIPT_FILENAME'] points to a script that doesn't exist. E.g. functional testing. In this |
||
828 | * case, realpath() returns false and str_replace an empty string, so we compare them loosely. |
||
829 | * |
||
830 | * - The URI requested does not belong to a script in the www/ directory of SimpleSAMLphp. In that case, |
||
831 | * removing SimpleSAMLphp's base dir from the current path yields the same path, so $cur_path and |
||
832 | * $rel_path are equal. |
||
833 | * |
||
834 | * - The request URI does not match the current script. Even if the current script is located in the www/ |
||
835 | * directory of SimpleSAMLphp, the URI does not contain its relative path, and $uri_pos is false. |
||
836 | * |
||
837 | * It doesn't matter which one of those cases we have. We just know we can't apply our base URL to the |
||
838 | * current URI, so we need to build it back from the PHP environment, unless we have a base URL specified |
||
839 | * for this case in the configuration. First, check if that's the case. |
||
840 | */ |
||
841 | |||
842 | /** @var \SimpleSAML\Configuration $appcfg */ |
||
843 | $appcfg = $cfg->getConfigItem('application'); |
||
844 | $appurl = $appcfg->getString('baseURL', ''); |
||
845 | if (!empty($appurl)) { |
||
846 | $protocol = parse_url($appurl, PHP_URL_SCHEME); |
||
847 | $hostname = parse_url($appurl, PHP_URL_HOST); |
||
848 | $port = parse_url($appurl, PHP_URL_PORT); |
||
849 | $port = !empty($port) ? ':' . $port : ''; |
||
850 | } else { |
||
851 | // no base URL specified for app, just use the current URL |
||
852 | $protocol = self::getServerHTTPS() ? 'https' : 'http'; |
||
853 | $hostname = self::getServerHost(); |
||
854 | $port = self::getServerPort(); |
||
855 | } |
||
856 | return $protocol . '://' . $hostname . $port . $_SERVER['REQUEST_URI']; |
||
857 | } |
||
858 | |||
859 | return self::getBaseURL() . $url_path . substr($_SERVER['REQUEST_URI'], $uri_pos + strlen($url_path)); |
||
860 | } |
||
861 | |||
862 | |||
863 | /** |
||
864 | * Retrieve the current URL using the base URL in the configuration, containing the protocol, the host and |
||
865 | * optionally, the port number. |
||
866 | * |
||
867 | * @return string The current URL without path or query parameters. |
||
868 | * |
||
869 | * @author Andreas Solberg, UNINETT AS <[email protected]> |
||
870 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
871 | */ |
||
872 | public static function getSelfURLHost() |
||
873 | { |
||
874 | $url = self::getSelfURL(); |
||
875 | |||
876 | /** @var int $colon getBaseURL() will allways return a valid URL */ |
||
877 | $colon = strpos($url, '://'); |
||
878 | $start = $colon + 3; |
||
879 | $length = strcspn($url, '/', $start) + $start; |
||
880 | return substr($url, 0, $length); |
||
881 | } |
||
882 | |||
883 | |||
884 | /** |
||
885 | * Retrieve the current URL using the base URL in the configuration, without the query parameters. |
||
886 | * |
||
887 | * @return string The current URL, not including query parameters. |
||
888 | * |
||
889 | * @author Andreas Solberg, UNINETT AS <[email protected]> |
||
890 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
891 | */ |
||
892 | public static function getSelfURLNoQuery() |
||
893 | { |
||
894 | $url = self::getSelfURL(); |
||
895 | $pos = strpos($url, '?'); |
||
896 | if (!$pos) { |
||
897 | return $url; |
||
898 | } |
||
899 | return substr($url, 0, $pos); |
||
900 | } |
||
901 | |||
902 | |||
903 | /** |
||
904 | * This function checks if we are using HTTPS as protocol. |
||
905 | * |
||
906 | * @return boolean True if the HTTPS is used, false otherwise. |
||
907 | * |
||
908 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
909 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
910 | */ |
||
911 | public static function isHTTPS() |
||
912 | { |
||
913 | return strpos(self::getSelfURL(), 'https://') === 0; |
||
914 | } |
||
915 | |||
916 | |||
917 | /** |
||
918 | * Normalizes a URL to an absolute URL and validate it. In addition to resolving the URL, this function makes sure |
||
919 | * that it is a link to an http or https site. |
||
920 | * |
||
921 | * @param string $url The relative URL. |
||
922 | * |
||
923 | * @return string An absolute URL for the given relative URL. |
||
924 | * @throws \InvalidArgumentException If $url is not a string or a valid URL. |
||
925 | * |
||
926 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
927 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
928 | */ |
||
929 | public static function normalizeURL($url) |
||
930 | { |
||
931 | if (!is_string($url)) { |
||
932 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
933 | } |
||
934 | |||
935 | $url = self::resolveURL($url, self::getSelfURL()); |
||
936 | |||
937 | // verify that the URL is to a http or https site |
||
938 | if (!preg_match('@^https?://@i', $url)) { |
||
939 | throw new \InvalidArgumentException('Invalid URL: ' . $url); |
||
940 | } |
||
941 | |||
942 | return $url; |
||
943 | } |
||
944 | |||
945 | |||
946 | /** |
||
947 | * Parse a query string into an array. |
||
948 | * |
||
949 | * This function parses a query string into an array, similar to the way the builtin 'parse_str' works, except it |
||
950 | * doesn't handle arrays, and it doesn't do "magic quotes". |
||
951 | * |
||
952 | * Query parameters without values will be set to an empty string. |
||
953 | * |
||
954 | * @param string $query_string The query string which should be parsed. |
||
955 | * |
||
956 | * @return array The query string as an associative array. |
||
957 | * @throws \InvalidArgumentException If $query_string is not a string. |
||
958 | * |
||
959 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
960 | */ |
||
961 | public static function parseQueryString($query_string) |
||
962 | { |
||
963 | if (!is_string($query_string)) { |
||
964 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
965 | } |
||
966 | |||
967 | $res = []; |
||
968 | if (empty($query_string)) { |
||
969 | return $res; |
||
970 | } |
||
971 | |||
972 | foreach (explode('&', $query_string) as $param) { |
||
973 | $param = explode('=', $param); |
||
974 | $name = urldecode($param[0]); |
||
975 | if (count($param) === 1) { |
||
976 | $value = ''; |
||
977 | } else { |
||
978 | $value = urldecode($param[1]); |
||
979 | } |
||
980 | $res[$name] = $value; |
||
981 | } |
||
982 | return $res; |
||
983 | } |
||
984 | |||
985 | |||
986 | /** |
||
987 | * This function redirects to the specified URL without performing any security checks. Please, do NOT use this |
||
988 | * function with user supplied URLs. |
||
989 | * |
||
990 | * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the |
||
991 | * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used. |
||
992 | * |
||
993 | * The function will also generate a simple web page with a clickable link to the target URL. |
||
994 | * |
||
995 | * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a |
||
996 | * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute |
||
997 | * URL to the root of the website. |
||
998 | * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The |
||
999 | * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the |
||
1000 | * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the |
||
1001 | * name, without a value. |
||
1002 | * |
||
1003 | * @return void This function never returns. |
||
1004 | * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. |
||
1005 | * |
||
1006 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
1007 | */ |
||
1008 | public static function redirectTrustedURL($url, $parameters = []) |
||
1009 | { |
||
1010 | if (!is_string($url) || !is_array($parameters)) { |
||
1011 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
1012 | } |
||
1013 | |||
1014 | $url = self::normalizeURL($url); |
||
1015 | self::redirect($url, $parameters); |
||
1016 | } |
||
1017 | |||
1018 | |||
1019 | /** |
||
1020 | * This function redirects to the specified URL after performing the appropriate security checks on it. |
||
1021 | * Particularly, it will make sure that the provided URL is allowed by the 'trusted.url.domains' directive in the |
||
1022 | * configuration. |
||
1023 | * |
||
1024 | * If the aforementioned option is not set or the URL does correspond to a trusted site, it performs a redirection |
||
1025 | * to it. If the site is not trusted, an exception will be thrown. |
||
1026 | * |
||
1027 | * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a |
||
1028 | * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute |
||
1029 | * URL to the root of the website. |
||
1030 | * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The |
||
1031 | * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the |
||
1032 | * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the |
||
1033 | * name, without a value. |
||
1034 | * |
||
1035 | * @return void This function never returns. |
||
1036 | * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. |
||
1037 | * |
||
1038 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
1039 | */ |
||
1040 | public static function redirectUntrustedURL($url, $parameters = []) |
||
1041 | { |
||
1042 | if (!is_string($url) || !is_array($parameters)) { |
||
1043 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
1044 | } |
||
1045 | |||
1046 | $url = self::checkURLAllowed($url); |
||
1047 | self::redirect($url, $parameters); |
||
1048 | } |
||
1049 | |||
1050 | |||
1051 | /** |
||
1052 | * Resolve a (possibly relative) URL relative to a given base URL. |
||
1053 | * |
||
1054 | * This function supports these forms of relative URLs: |
||
1055 | * - ^\w+: Absolute URL. E.g. "http://www.example.com:port/path?query#fragment". |
||
1056 | * - ^// Same protocol. E.g. "//www.example.com:port/path?query#fragment" |
||
1057 | * - ^/ Same protocol and host. E.g. "/path?query#fragment". |
||
1058 | * - ^? Same protocol, host and path, replace query string & fragment. E.g. "?query#fragment". |
||
1059 | * - ^# Same protocol, host, path and query, replace fragment. E.g. "#fragment". |
||
1060 | * - The rest: Relative to the base path. |
||
1061 | * |
||
1062 | * @param string $url The relative URL. |
||
1063 | * @param string $base The base URL. Defaults to the base URL of this installation of SimpleSAMLphp. |
||
1064 | * |
||
1065 | * @return string An absolute URL for the given relative URL. |
||
1066 | * @throws \InvalidArgumentException If the base URL cannot be parsed into a valid URL, or the given parameters |
||
1067 | * are not strings. |
||
1068 | * |
||
1069 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
1070 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
1071 | */ |
||
1072 | public static function resolveURL($url, $base = null) |
||
1073 | { |
||
1074 | if ($base === null) { |
||
1075 | $base = self::getBaseURL(); |
||
1076 | } |
||
1077 | |||
1078 | if (!is_string($url) || !is_string($base)) { |
||
1079 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
1080 | } |
||
1081 | |||
1082 | if (!preg_match('/^((((\w+:)\/\/[^\/]+)(\/[^?#]*))(?:\?[^#]*)?)(?:#.*)?/', $base, $baseParsed)) { |
||
1083 | throw new \InvalidArgumentException('Unable to parse base url: ' . $base); |
||
1084 | } |
||
1085 | |||
1086 | $baseDir = dirname($baseParsed[5] . 'filename'); |
||
1087 | $baseScheme = $baseParsed[4]; |
||
1088 | $baseHost = $baseParsed[3]; |
||
1089 | $basePath = $baseParsed[2]; |
||
1090 | $baseQuery = $baseParsed[1]; |
||
1091 | |||
1092 | if (preg_match('$^\w+:$', $url)) { |
||
1093 | return $url; |
||
1094 | } |
||
1095 | |||
1096 | if (substr($url, 0, 2) === '//') { |
||
1097 | return $baseScheme . $url; |
||
1098 | } |
||
1099 | |||
1100 | if ($url[0] === '/') { |
||
1101 | return $baseHost . $url; |
||
1102 | } |
||
1103 | if ($url[0] === '?') { |
||
1104 | return $basePath . $url; |
||
1105 | } |
||
1106 | if ($url[0] === '#') { |
||
1107 | return $baseQuery . $url; |
||
1108 | } |
||
1109 | |||
1110 | // we have a relative path. Remove query string/fragment and save it as $tail |
||
1111 | $queryPos = strpos($url, '?'); |
||
1112 | $fragmentPos = strpos($url, '#'); |
||
1113 | if ($queryPos !== false || $fragmentPos !== false) { |
||
1114 | if ($queryPos === false) { |
||
1115 | $tailPos = $fragmentPos; |
||
1116 | } elseif ($fragmentPos === false) { |
||
1117 | $tailPos = $queryPos; |
||
1118 | } elseif ($queryPos < $fragmentPos) { |
||
1119 | $tailPos = $queryPos; |
||
1120 | } else { |
||
1121 | $tailPos = $fragmentPos; |
||
1122 | } |
||
1123 | |||
1124 | $tail = substr($url, $tailPos); |
||
1125 | $dir = substr($url, 0, $tailPos); |
||
1126 | } else { |
||
1127 | $dir = $url; |
||
1128 | $tail = ''; |
||
1129 | } |
||
1130 | |||
1131 | $dir = System::resolvePath($dir, $baseDir); |
||
1132 | |||
1133 | return $baseHost . $dir . $tail; |
||
1134 | } |
||
1135 | |||
1136 | |||
1137 | /** |
||
1138 | * Set a cookie. |
||
1139 | * |
||
1140 | * @param string $name The name of the cookie. |
||
1141 | * @param string|NULL $value The value of the cookie. Set to NULL to delete the cookie. |
||
1142 | * @param array|NULL $params Cookie parameters. |
||
1143 | * @param bool $throw Whether to throw exception if setcookie() fails. |
||
1144 | * |
||
1145 | * @throws \InvalidArgumentException If any parameter has an incorrect type. |
||
1146 | * @throws \SimpleSAML\Error\CannotSetCookie If the headers were already sent and the cookie cannot be set. |
||
1147 | * |
||
1148 | * @return void |
||
1149 | * |
||
1150 | * @author Andjelko Horvat |
||
1151 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
1152 | */ |
||
1153 | public static function setCookie($name, $value, $params = null, $throw = true) |
||
1154 | { |
||
1155 | if ( |
||
1156 | !(is_string($name) // $name must be a string |
||
1157 | && (is_string($value) |
||
1158 | || is_null($value)) // $value can be a string or null |
||
1159 | && (is_array($params) |
||
1160 | || is_null($params)) // $params can be an array or null |
||
1161 | && is_bool($throw)) // $throw must be boolean |
||
1162 | ) { |
||
1163 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
1164 | } |
||
1165 | |||
1166 | $default_params = [ |
||
1167 | 'lifetime' => 0, |
||
1168 | 'expire' => null, |
||
1169 | 'path' => '/', |
||
1170 | 'domain' => '', |
||
1171 | 'secure' => false, |
||
1172 | 'httponly' => true, |
||
1173 | 'raw' => false, |
||
1174 | 'samesite' => null, |
||
1175 | ]; |
||
1176 | |||
1177 | if ($params !== null) { |
||
1178 | $params = array_merge($default_params, $params); |
||
1179 | } else { |
||
1180 | $params = $default_params; |
||
1181 | } |
||
1182 | |||
1183 | // Do not set secure cookie if not on HTTPS |
||
1184 | if ($params['secure'] && !self::isHTTPS()) { |
||
1185 | if ($throw) { |
||
1186 | throw new Error\CannotSetCookie( |
||
1187 | 'Setting secure cookie on plain HTTP is not allowed.', |
||
1188 | Error\CannotSetCookie::SECURE_COOKIE |
||
1189 | ); |
||
1190 | } |
||
1191 | Logger::warning('Error setting cookie: setting secure cookie on plain HTTP is not allowed.'); |
||
1192 | return; |
||
1193 | } |
||
1194 | |||
1195 | if ($value === null) { |
||
1196 | $expire = time() - 365 * 24 * 60 * 60; |
||
1197 | $value = strval($value); |
||
1198 | } elseif (isset($params['expire'])) { |
||
1199 | $expire = intval($params['expire']); |
||
1200 | } elseif ($params['lifetime'] === 0) { |
||
1201 | $expire = 0; |
||
1202 | } else { |
||
1203 | $expire = time() + intval($params['lifetime']); |
||
1204 | } |
||
1205 | |||
1206 | if (version_compare(PHP_VERSION, '7.3.0', '>=')) { |
||
1207 | /* use the new options array for PHP >= 7.3 */ |
||
1208 | if ($params['raw']) { |
||
1209 | /** @psalm-suppress InvalidArgument Remove when Psalm >= 3.4.10 */ |
||
1210 | $success = @setrawcookie( |
||
1211 | $name, |
||
1212 | $value, |
||
1213 | [ |
||
1214 | 'expires' => $expire, |
||
1215 | 'path' => $params['path'], |
||
1216 | 'domain' => $params['domain'], |
||
1217 | 'secure' => $params['secure'], |
||
1218 | 'httponly' => $params['httponly'], |
||
1219 | 'samesite' => $params['samesite'], |
||
1220 | ] |
||
1221 | ); |
||
1222 | } else { |
||
1223 | /** @psalm-suppress InvalidArgument Remove when Psalm >= 3.4.10 */ |
||
1224 | $success = @setcookie( |
||
1225 | $name, |
||
1226 | $value, |
||
1227 | [ |
||
1228 | 'expires' => $expire, |
||
1229 | 'path' => $params['path'], |
||
1230 | 'domain' => $params['domain'], |
||
1231 | 'secure' => $params['secure'], |
||
1232 | 'httponly' => $params['httponly'], |
||
1233 | 'samesite' => $params['samesite'], |
||
1234 | ] |
||
1235 | ); |
||
1236 | } |
||
1237 | } else { |
||
1238 | /* in older versions of PHP we need a nasty hack to set RFC6265bis SameSite attribute */ |
||
1239 | if ($params['samesite'] !== null && !preg_match('/;\s+samesite/i', $params['path'])) { |
||
1240 | $params['path'] .= '; SameSite=' . $params['samesite']; |
||
1241 | } |
||
1242 | if ($params['raw']) { |
||
1243 | $success = @setrawcookie( |
||
1244 | $name, |
||
1245 | $value, |
||
1246 | $expire, |
||
1247 | $params['path'], |
||
1248 | $params['domain'], |
||
1249 | $params['secure'], |
||
1250 | $params['httponly'] |
||
1251 | ); |
||
1252 | } else { |
||
1253 | $success = @setcookie( |
||
1254 | $name, |
||
1255 | $value, |
||
1256 | $expire, |
||
1257 | $params['path'], |
||
1258 | $params['domain'], |
||
1259 | $params['secure'], |
||
1260 | $params['httponly'] |
||
1261 | ); |
||
1262 | } |
||
1263 | } |
||
1264 | |||
1265 | if (!$success) { |
||
1266 | if ($throw) { |
||
1267 | throw new Error\CannotSetCookie( |
||
1268 | 'Headers already sent.', |
||
1269 | Error\CannotSetCookie::HEADERS_SENT |
||
1270 | ); |
||
1271 | } |
||
1272 | Logger::warning('Error setting cookie: headers already sent.'); |
||
1273 | } |
||
1274 | } |
||
1275 | |||
1276 | |||
1277 | /** |
||
1278 | * Submit a POST form to a specific destination. |
||
1279 | * |
||
1280 | * This function never returns. |
||
1281 | * |
||
1282 | * @param string $destination The destination URL. |
||
1283 | * @param array $data An associative array with the data to be posted to $destination. |
||
1284 | * |
||
1285 | * @throws \InvalidArgumentException If $destination is not a string or $data is not an array. |
||
1286 | * @throws \SimpleSAML\Error\Exception If $destination is not a valid HTTP URL. |
||
1287 | * |
||
1288 | * @return void |
||
1289 | * |
||
1290 | * @author Olav Morken, UNINETT AS <[email protected]> |
||
1291 | * @author Andjelko Horvat |
||
1292 | * @author Jaime Perez, UNINETT AS <[email protected]> |
||
1293 | */ |
||
1294 | public static function submitPOSTData($destination, $data) |
||
1295 | { |
||
1296 | if (!is_string($destination) || !is_array($data)) { |
||
1297 | throw new \InvalidArgumentException('Invalid input parameters.'); |
||
1298 | } |
||
1299 | if (!self::isValidURL($destination)) { |
||
1300 | throw new Error\Exception('Invalid destination URL.'); |
||
1301 | } |
||
1302 | |||
1303 | $config = Configuration::getInstance(); |
||
1304 | $allowed = $config->getBoolean('enable.http_post', false); |
||
1305 | |||
1306 | if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) { |
||
1307 | // we need to post the data to HTTP |
||
1308 | self::redirect(self::getSecurePOSTRedirectURL($destination, $data)); |
||
1309 | } |
||
1310 | |||
1311 | $p = new Template($config, 'post.php'); |
||
1312 | $p->data['destination'] = $destination; |
||
1313 | $p->data['post'] = $data; |
||
1314 | $p->show(); |
||
1315 | exit(0); |
||
1316 | } |
||
1317 | } |
||
1318 |
'Location: ' . $url
can contain request data and is used in response header context(s) leading to a potential security vulnerability.1 path for user data to reach this point
$_REQUEST,
and HTTP::redirectUntrustedURL() is calledin modules/core/www/login-admin.php on line 12
$url
in lib/SimpleSAML/Utils/HTTP.php on line 1040
checkURLAllowed()
, andself::checkURLAllowed($url)
is assigned to$url
in lib/SimpleSAML/Utils/HTTP.php on line 1046
in lib/SimpleSAML/Utils/HTTP.php on line 1047
$url
in lib/SimpleSAML/Utils/HTTP.php on line 176
Response Splitting Attacks
Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.
General Strategies to prevent injection
In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:
For numeric data, we recommend to explicitly cast the data: