image url curl resolve

This commit is contained in:
Yurii
2026-03-22 23:31:11 +02:00
parent 27b1ce882b
commit dca03cc345
3 changed files with 154 additions and 10 deletions

View File

@@ -50,6 +50,31 @@ class HostCheck
return $this->ipAddressIsNotInternal($host);
}
if (!$this->isDomainHost($host)) {
return false;
}
$ipAddresses = $this->getHostIpAddresses($host);
if ($ipAddresses === []) {
return false;
}
foreach ($ipAddresses as $idAddress) {
if (!$this->ipAddressIsNotInternal($idAddress)) {
return false;
}
}
return true;
}
/**
* @internal
* @since 9.3.4
*/
public function isDomainHost(string $host): bool
{
$normalized = $this->normalizeIpAddress($host);
if ($normalized !== false && filter_var($normalized, FILTER_VALIDATE_IP)) {
@@ -64,29 +89,46 @@ class HostCheck
return false;
}
if (filter_var($host, FILTER_VALIDATE_DOMAIN)) {
return true;
}
return false;
}
/**
* @return string[]
* @internal
* @since 9.3.4
*/
public function getHostIpAddresses(string $host): array
{
$records = dns_get_record($host, DNS_A);
if (!$records) {
return true;
return [];
}
$output = [];
foreach ($records as $record) {
/** @var ?string $idAddress */
$idAddress = $record['ip'] ?? null;
if (!$idAddress) {
return false;
continue;
}
if (!$this->ipAddressIsNotInternal($idAddress)) {
return false;
}
$output[] = $idAddress;
}
return true;
return $output;
}
private function ipAddressIsNotInternal(string $ipAddress): bool
/**
* @internal
*/
public function ipAddressIsNotInternal(string $ipAddress): bool
{
return (bool) filter_var(
$ipAddress,

View File

@@ -29,9 +29,6 @@
namespace Espo\Core\Utils\Security;
use const FILTER_VALIDATE_URL;
use const PHP_URL_HOST;
class UrlCheck
{
public function __construct(
@@ -63,6 +60,65 @@ class UrlCheck
return $this->hostCheck->isHostAndNotInternal($host);
}
/**
* @return ?string[] Null if not a domain name or not a URL.
* @internal
* @since 9.3.4
*/
public function getCurlResolve(string $url): ?array
{
if (!$this->isUrl($url)) {
return null;
}
$host = parse_url($url, PHP_URL_HOST);
$port = parse_url($url, PHP_URL_PORT);
$scheme = parse_url($url, PHP_URL_SCHEME);
if ($port === null && $scheme) {
$port = match (strtolower($scheme)) {
'http' => 80,
'https'=> 443,
'ftp' => 21,
'ssh' => 22,
'smtp' => 25,
default => null,
};
}
if ($port === null) {
return [];
}
if (!is_string($host)) {
return null;
}
if (filter_var($host, FILTER_VALIDATE_IP)) {
return null;
}
if (!$this->hostCheck->isDomainHost($host)) {
return null;
}
$ipAddresses = $this->hostCheck->getHostIpAddresses($host);
$output = [];
foreach ($ipAddresses as $ipAddress) {
$ipPart = $ipAddress;
if (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ipPart = "[$ipPart]";
}
$output[] = "$host:$port:$ipPart";
}
return $output;
}
/**
* @deprecated Since 9.3.4. Use `isUrlAndNotIternal`.
* @todo Remove in 9.5.0.
@@ -71,4 +127,35 @@ class UrlCheck
{
return $this->isUrlAndNotIternal($url);
}
/**
* @param string[] $resolve
* @internal
*/
public function validateCurlResolveNotInternal(array $resolve): bool
{
if ($resolve === []) {
return false;
}
foreach ($resolve as $item) {
$arr = explode(':', $item, 3);
if (count($arr) < 3) {
return false;
}
$ipAddress = $arr[2];
if (str_starts_with($ipAddress, '[') && str_ends_with($ipAddress, ']')) {
$ipAddress = substr($ipAddress, 1, -1);
}
if (!$this->hostCheck->ipAddressIsNotInternal($ipAddress)) {
return false;
}
}
return true;
}
}

View File

@@ -114,9 +114,20 @@ class UploadUrlService
/**
* @param non-empty-string $url
* @return ?array{string, string} A type and contents.
* @throws ForbiddenSilent
*/
private function getImageDataByUrl(string $url): ?array
{
$resolve = $this->urlCheck->getCurlResolve($url);
if ($resolve === []) {
throw new ForbiddenSilent("Could not resolve the host.");
}
if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve)) {
throw new ForbiddenSilent("Forbidden host.");
}
$type = null;
if (!function_exists('curl_init')) {
@@ -144,6 +155,10 @@ class UploadUrlService
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
if ($resolve) {
$opts[CURLOPT_RESOLVE] = $resolve;
}
$ch = curl_init();
curl_setopt_array($ch, $opts);