mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-07-16 05:33:49 +03:00
Added a central URLFilter class to check & clean URLs used for attachments, which is also used for validation, and by the purifier to standardise protocols (and to make protocol config easier in future). Thanks to mfk25 for reporting.
104 lines
2.9 KiB
PHP
104 lines
2.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace BookStack\Util;
|
|
|
|
/**
|
|
* Helps filter URLs to prevent use of undesired schemes.
|
|
* Also parses and rebuilds the URL to ensure it's valid.
|
|
*/
|
|
class UrlFilter
|
|
{
|
|
protected static array $allowedSchemes = ['http', 'https', 'mailto', 'tel', 'file', 'ftp', 'nntp', 'news'];
|
|
protected string $url;
|
|
|
|
public function __construct(string $url)
|
|
{
|
|
$this->url = trim($url);
|
|
}
|
|
|
|
/**
|
|
* Check if the URL is allowed to be generally used as a link
|
|
* in the application. This does not assure the original URL string
|
|
* provided is safe as-is. Ensure you use the clean method to produce
|
|
* a URL that is considered safe to use.
|
|
*/
|
|
public function isAllowed(): bool
|
|
{
|
|
$urlParts = parse_url($this->url);
|
|
if (!$urlParts) {
|
|
return false;
|
|
}
|
|
|
|
// Extra check to help avoid scenarios where non-standard characters are used in the scheme
|
|
// to work around parse_url handling with URLs which may be interpreted by the browser differently.
|
|
if (str_contains($this->url, ':') && !preg_match('/^[a-z]+:/i', $this->url)) {
|
|
return false;
|
|
}
|
|
|
|
if (isset($urlParts['scheme'])) {
|
|
return in_array(strtolower($urlParts['scheme']), self::$allowedSchemes);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Clean the URL to ensure it's valid and only uses the allowed schemes.
|
|
* If the URL is not allowed, return a placeholder.
|
|
*/
|
|
public function clean(): string
|
|
{
|
|
if (!$this->isAllowed()) {
|
|
return '#badlink';
|
|
}
|
|
|
|
$urlParts = parse_url($this->url);
|
|
if (!$urlParts) {
|
|
return '#badlink';
|
|
}
|
|
|
|
$url = '';
|
|
|
|
if (isset($urlParts['scheme']) || isset($urlParts['host'])) {
|
|
$scheme = strtolower($urlParts['scheme'] ?? 'https');
|
|
$url = $scheme . ':' . (isset($urlParts['host']) ? '//' : '');
|
|
}
|
|
|
|
if (isset($urlParts['user']) || isset($urlParts['pass'])) {
|
|
$url .= $urlParts['user'] ?? '';
|
|
if (isset($urlParts['pass'])) {
|
|
$url .= ':' . $urlParts['pass'];
|
|
}
|
|
$url .= '@';
|
|
}
|
|
|
|
if (isset($urlParts['host'])) {
|
|
$url .= $urlParts['host'];
|
|
}
|
|
if (isset($urlParts['port'])) {
|
|
$url .= ':' . $urlParts['port'];
|
|
}
|
|
if (isset($urlParts['path'])) {
|
|
$url .= $urlParts['path'];
|
|
}
|
|
if (isset($urlParts['query'])) {
|
|
$url .= '?' . $urlParts['query'];
|
|
}
|
|
if (isset($urlParts['fragment'])) {
|
|
$url .= '#' . $urlParts['fragment'];
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Get schemes that are allowed to be used in content links.
|
|
*/
|
|
public static function getAllowedSchemes(): array
|
|
{
|
|
return self::$allowedSchemes;
|
|
}
|
|
}
|