From 50f07e2da1c04f91c04b22603866b8536537bef1 Mon Sep 17 00:00:00 2001 From: Yurii Date: Mon, 23 Mar 2026 09:28:27 +0200 Subject: [PATCH] url check webhook --- .../Espo/Core/Utils/Security/UrlCheck.php | 15 ++++++++++++++- application/Espo/Core/Webhook/Sender.php | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/application/Espo/Core/Utils/Security/UrlCheck.php b/application/Espo/Core/Utils/Security/UrlCheck.php index 2a12ea00fc..8ddd937173 100644 --- a/application/Espo/Core/Utils/Security/UrlCheck.php +++ b/application/Espo/Core/Utils/Security/UrlCheck.php @@ -130,14 +130,17 @@ class UrlCheck /** * @param string[] $resolve + * @param string[] $allowed An allowed address list in the `{host}:{port}` format. * @internal */ - public function validateCurlResolveNotInternal(array $resolve): bool + public function validateCurlResolveNotInternal(array $resolve, array $allowed = []): bool { if ($resolve === []) { return false; } + $ipAddresses = []; + foreach ($resolve as $item) { $arr = explode(':', $item, 3); @@ -146,11 +149,21 @@ class UrlCheck } $ipAddress = $arr[2]; + $port = $arr[1]; + $domain = $arr[0]; + + if (in_array("$ipAddress:$port", $allowed) || in_array("$domain:$port", $allowed)) { + return true; + } if (str_starts_with($ipAddress, '[') && str_ends_with($ipAddress, ']')) { $ipAddress = substr($ipAddress, 1, -1); } + $ipAddresses[] = $ipAddress; + } + + foreach ($ipAddresses as $ipAddress) { if (!$this->hostCheck->ipAddressIsNotInternal($ipAddress)) { return false; } diff --git a/application/Espo/Core/Webhook/Sender.php b/application/Espo/Core/Webhook/Sender.php index b810449033..55a7827840 100644 --- a/application/Espo/Core/Webhook/Sender.php +++ b/application/Espo/Core/Webhook/Sender.php @@ -100,6 +100,19 @@ class Sender throw new Error("URL '$url' points to an internal host, not allowed."); } + $resolve = $this->urlCheck->getCurlResolve($url); + + if ($resolve === []) { + throw new Error("Could not resolve the host."); + } + + /** @var string[] $allowedAddressList */ + $allowedAddressList = $this->config->get('webhookAllowedAddressList') ?? []; + + if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve, $allowedAddressList)) { + throw new Error("Forbidden host."); + } + $handler = curl_init($url); if ($handler === false) { @@ -118,6 +131,10 @@ class Sender curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList); curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload); + if ($resolve) { + curl_setopt($handler, CURLOPT_RESOLVE, $resolve); + } + curl_exec($handler); $code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);