From 7831b84cc46c9609b7a328cda4706ee4aaa76ed0 Mon Sep 17 00:00:00 2001 From: djmaze <> Date: Fri, 23 Oct 2026 17:09:37 +0200 Subject: [PATCH 1/2] Resolve SSRF GHSA-xpr6-5vj6-wq26 --- .../app/libraries/RainLoop/ServiceActions.php | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index 26ab6e6a0..c2ef623c9 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -276,6 +276,30 @@ class ServiceActions return $this->privateUpload('UploadBackground', 1); } + // https://github.com/the-djmaze/snappymail/security/advisories/GHSA-xpr6-5vj6-wq26 + private static function allowedProxyUri($uri) : bool + { + if (!\preg_match('#^https?://([^/:]+)#i', $uri, $m)) { + return false; + } + $host = $m[1]; + // Validate direct IP or resolve hostname + $ips = \filter_var($host, \FILTER_VALIDATE_IP) ? [$host] : \gethostbynamel($host); + if (!$ips || !\is_array($ips)) { + return false; + } + foreach ($ips as $ip) { + if (!\filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE)) { + return false; + } + // Explicitly reject loopback, link-local, and cloud metadata addresses + if (\str_starts_with($ip, '127.') || \str_starts_with($ip, '169.254.') || '::1' === $ip || '0.0.0.0' === $ip) { + return false; + } + } + return true; + } + public function ServiceProxyExternal() : string { $sData = empty($this->aPaths[1]) ? '' : $this->aPaths[1]; @@ -285,7 +309,7 @@ class ServiceActions ) { $this->oActions->verifyCacheByKey($sData); $sUrl = \MailSo\Base\Utils::UrlSafeBase64Decode($sData); - if (!empty($sUrl)) { + if (!empty($sUrl) && static::allowedProxyUri($sUrl)) { \header('X-Content-Location: '.$sUrl); $tmp = \tmpfile(); $HTTP = \SnappyMail\HTTP\Request::factory(); From c9a5348193bd8405fe413dfe265e2fdc419f578c Mon Sep 17 00:00:00 2001 From: djmaze <> Date: Mon, 26 Oct 2026 00:23:08 +0100 Subject: [PATCH 2/2] Also check domainname for valid ip address --- .../0.0.0/app/libraries/RainLoop/ServiceActions.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index c2ef623c9..f853f4df7 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -279,10 +279,18 @@ class ServiceActions // https://github.com/the-djmaze/snappymail/security/advisories/GHSA-xpr6-5vj6-wq26 private static function allowedProxyUri($uri) : bool { - if (!\preg_match('#^https?://([^/:]+)#i', $uri, $m)) { + if (!\preg_match('#^https?://#i', $uri)) { return false; } - $host = $m[1]; + $host = \parse_url($uri, \PHP_URL_HOST); + if (!\is_string($host) || '' === $host) { + return null; + } + // Bracketed IPv6 literals (http://[::1]/): parse_url keeps the + // brackets, which filter_var rejects, so strip them first. + if (\str_starts_with($host, '[') && \str_ends_with($host, ']')) { + $host = \substr($host, 1, -1); + } // Validate direct IP or resolve hostname $ips = \filter_var($host, \FILTER_VALIDATE_IP) ? [$host] : \gethostbynamel($host); if (!$ips || !\is_array($ips)) {