Merge remote-tracking branch 'ghsa-xpr6-5vj6-wq26/advisory-fix-1'

This commit is contained in:
djmaze 2026-11-03 15:28:00 +01:00
commit 037861ba5a

View file

@ -282,6 +282,38 @@ 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)) {
return false;
}
$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)) {
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];
@ -291,7 +323,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();