From c536cee6375e2088f961af13db7aaa652c983072 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 6 Nov 2023 16:02:13 +0200 Subject: [PATCH] image upload url check --- .../Espo/Core/Utils/Security/UrlCheck.php | 95 +++++++++++++++++++ .../Tools/Attachment/UploadUrlService.php | 14 ++- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 application/Espo/Core/Utils/Security/UrlCheck.php diff --git a/application/Espo/Core/Utils/Security/UrlCheck.php b/application/Espo/Core/Utils/Security/UrlCheck.php new file mode 100644 index 0000000000..bb1323548b --- /dev/null +++ b/application/Espo/Core/Utils/Security/UrlCheck.php @@ -0,0 +1,95 @@ +isUrl($url)) { + return false; + } + + $host = parse_url($url, PHP_URL_HOST); + + if (!is_string($host)) { + return false; + } + + $records = dns_get_record($host, DNS_A); + + if (filter_var($host, FILTER_VALIDATE_IP)) { + return $this->ipAddressIsNotInternal($host); + } + + if (!$records) { + return false; + } + + foreach ($records as $record) { + /** @var ?string $idAddress */ + $idAddress = $record['ip'] ?? null; + + if (!$idAddress) { + return false; + } + + if (!$this->ipAddressIsNotInternal($idAddress)) { + return false; + } + } + + return true; + } + + private function ipAddressIsNotInternal(string $ipAddress): bool + { + return (bool) filter_var( + $ipAddress, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE + ); + } +} diff --git a/application/Espo/Tools/Attachment/UploadUrlService.php b/application/Espo/Tools/Attachment/UploadUrlService.php index 7dbeec065c..d1d8566b66 100644 --- a/application/Espo/Tools/Attachment/UploadUrlService.php +++ b/application/Espo/Tools/Attachment/UploadUrlService.php @@ -32,8 +32,10 @@ namespace Espo\Tools\Attachment; use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\ErrorSilent; use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Exceptions\ForbiddenSilent; use Espo\Core\Utils\File\MimeType; use Espo\Core\Utils\Metadata; +use Espo\Core\Utils\Security\UrlCheck; use Espo\Entities\Attachment as Attachment; use Espo\ORM\EntityManager; use Espo\Repositories\Attachment as AttachmentRepository; @@ -51,7 +53,8 @@ class UploadUrlService Metadata $metadata, EntityManager $entityManager, MimeType $mimeType, - DetailsObtainer $detailsObtainer + DetailsObtainer $detailsObtainer, + private UrlCheck $urlCheck ) { $this->accessChecker = $accessChecker; $this->metadata = $metadata; @@ -68,6 +71,10 @@ class UploadUrlService */ public function uploadImage(string $url, FieldData $data): Attachment { + if (!$this->urlCheck->isNotInternalUrl($url)) { + throw new ForbiddenSilent("Not allowed URL."); + } + $attachment = $this->getAttachmentRepository()->getNew(); $this->accessChecker->check($data); @@ -130,9 +137,12 @@ class UploadUrlService $opts[\CURLOPT_SSL_VERIFYPEER] = true; $opts[\CURLOPT_SSL_VERIFYHOST] = 2; $opts[\CURLOPT_RETURNTRANSFER] = true; - $opts[\CURLOPT_FOLLOWLOCATION] = true; + // Prevents Server Side Request Forgery by redirecting to an internal host. + $opts[\CURLOPT_FOLLOWLOCATION] = false; $opts[\CURLOPT_MAXREDIRS] = 2; $opts[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; + $opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP; + $opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS; $ch = curl_init();