Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions Sources/PackageManager/FtpConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@

namespace SMF\PackageManager;

use SMF\Url;
use SMF\WebFetch\WebFetchApi;

/**
* Class FtpConnection
* Simple FTP protocol implementation.
Expand Down Expand Up @@ -303,13 +300,6 @@ public function passive(): bool
return false;
}

// Let's just double check that...
if (!(WebFetchApi::makeSafe(new Url('ftp://' . $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4] . ':' . ($match[5] * 256 + $match[6]))) instanceof Url)) {
$this->error = 'bad_server';

return false;
}

// This is pretty simple - store it for later use ;).
$this->pasv = ['ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]];

Expand Down
2 changes: 1 addition & 1 deletion Sources/ProxyServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public function checkRequest(): bool
// Don't proxy our own resources.
|| $request->host === Url::create(Config::$boardurl)->host
// SSRF protection: don't proxy localhost, private or reserved IPs, etc.
|| ($request = WebFetchApi::makeSafe($request)) === null
|| !$request->isFetchSafe(['http', 'https'])
) {
return false;
}
Expand Down
37 changes: 30 additions & 7 deletions Sources/Subs-Compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -4005,9 +4005,7 @@ function iri_to_url(string $iri): string|bool
}

/**
* Checks whether a URL is safe to fetch from the server, and then returns
* either a version of the URL where the host has been resolved to a literal
* IP address, or else null if the URL was unsafe to fetch.
* Checks whether a URL is safe to fetch from the server.
*
* Rejects URLs whose scheme is not in the fetchable set, and URLs whose
* host resolves (or is) a non-global IP address: loopback, private,
Expand All @@ -4017,12 +4015,37 @@ function iri_to_url(string $iri): string|bool
* is also re-applied to each redirect target by the fetchers.
*
* @param string $url The URL to check.
* @return string|null A version of $url where the host has been resolved to
* a literal IP address, or else null if the URL was unsafe to fetch.
* @return bool Whether the URL is safe to fetch.
*/
function make_fetch_safe($url)
function is_fetch_safe($url)
{
return SMF\WebFetch\WebFetchApi::makeSafe($url);
return SMF\Url::create($url)->isFetchSafe(array_keys(SMF\WebFetch\WebFetchApi::$scheme_handlers));
}

/**
* Looks up the IP address(es) that the given URL's host resolves to.
*
* @param string $url The URL
* @return array The IP address(es).
*/
function get_ips_for_url($url)
{
return SMF\Url::create($url)->getIPs();
}

/**
* Checks whether the given URL resolves to the given IP address.
*
* If the URL resolves to multiple IP addresses, this function returns true
* if any of those IP addresses are the given one.
*
* @param string $url The URL
* @param string $ip The IP address.
* @return bool Whether this URL resolves to the given IP address.
*/
function url_resolves_to($url, $ip)
{
return SMF\Url::create($url)->resolvesTo(SMF\IP::create($ip));
}

/**
Expand Down
119 changes: 105 additions & 14 deletions Sources/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ class Url implements \Stringable
*/
protected $is_ascii;

/****************************
* Internal static properties
****************************/

/**
* @var array
*
* Cache for $this->getIPs()
*/
protected static array $ips;

/****************
* Public methods
****************/
Expand Down Expand Up @@ -526,26 +537,106 @@ public function proxied(): self
*/
public function getIPs(): array
{
// Resolve the host to its address(es). A literal IP resolves to itself.
$ips = [];
$is_ascii = $this->is_ascii;
$this->toAscii();
$ascii_host = $this->host;

if (!isset(self::$ips[$ascii_host])) {
// Resolve the host to its address(es). A literal IP resolves to itself.
self::$ips[$ascii_host] = [];

if (filter_var(trim($ascii_host, '[]'), FILTER_VALIDATE_IP)) {
self::$ips[$ascii_host][] = new IP(trim($ascii_host, '[]'));
} else {
$records = @dns_get_record($ascii_host, DNS_A | DNS_AAAA);

foreach ((array) $records as $record) {
if (!empty($record['ip'])) {
self::$ips[$ascii_host][] = new IP($record['ip']);
}

if (!empty($record['ipv6'])) {
self::$ips[$ascii_host][] = new IP($record['ipv6']);
}
}
}
}

if (filter_var(trim($this->host, '[]'), FILTER_VALIDATE_IP)) {
$ips[] = new IP(trim($this->host, '[]'));
} else {
$records = @dns_get_record($this->host, DNS_A | DNS_AAAA);
if (!$is_ascii) {
$this->toUtf8();
}

foreach ((array) $records as $record) {
if (!empty($record['ip'])) {
$ips[] = new IP($record['ip']);
}
return self::$ips[$ascii_host];
}

if (!empty($record['ipv6'])) {
$ips[] = new IP($record['ipv6']);
}
/**
* Checks whether this URL resolves to the given IP address.
*
* If this URL resolves to multiple IP addresses, this method returns true
* if any of those IP addresses are the given one.
*
* @param \SMF\IP $ip The IP address to check.
* @return bool Whether this URL resolves to the given IP address.
*/
public function resolvesTo(IP $ip): bool
{
foreach ($this->getIPs() as $known_ip) {
if ((string) $ip === (string) $known_ip) {
return true;
}
}

return $ips;
return false;
}

/**
* Checks whether it is safe for the server to fetch this URL.
*
* Rejects URLs whose scheme is not in the fetchable set, and URLs whose
* host resolves to (or is) a non-global IP address: loopback, private,
* link-local (incl. 169.254.0.0/16 cloud metadata), or other reserved
* ranges.
*
* @param array $allowed_schemes The URL schemes that the WebFetchApi is
* willing to use when fetching the content of this URL. If empty, any
* scheme that the WebFetchApi has a handler for is allowed.
* Default: []
* @return bool Whether this URL is safe to fetch.
*/
public function isFetchSafe(array $allowed_schemes = []): bool
{
if (empty($allowed_schemes)) {
$allowed_schemes = array_keys(WebFetchApi::$scheme_handlers);
}

$is_ascii = $this->is_ascii;

$this->toAscii();

if (
// Only known fetchable schemes.
empty($this->scheme)
|| !\in_array($this->scheme, $allowed_schemes)
// Must have a host.
|| empty($this->host)
// Reject reserved TLDs, since they are never in public DNS.
|| preg_match('/\b(?' . '>example|local(?' . '>host)?|onion|test|alt|in(?' . '>ternal|valid))$/', $this->host)
) {
$is_safe = false;
} else {
$ips = $this->getIPs();

$is_safe = $ips !== [] && $ips === array_filter(
$ips,
fn($ip) => $ip->isValid(FILTER_FLAG_GLOBAL_RANGE),
);
}

if (!$is_ascii) {
$this->toUtf8();
}

return $is_safe;
}

/**
Expand Down
Loading