From 08ca9d97bac8d53aed4d95bf80402a9deafd56d2 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 19 Jul 2026 11:45:54 -0400 Subject: [PATCH] fix: restrict image proxy to non-reserved addresses and monitor editors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit web/views/image.php exposes an outbound fetch via ?view=image&proxy=. It validated only the URL scheme, so any user with canView('Events') could make the server issue HTTP requests to arbitrary hosts and, on a 401 Digest challenge, replay credentials taken from the URL's user-info. view=image is also exempt from CSRF handling in index.php, so the fetch could be triggered from an attacker page via a plain tag. Camera discovery legitimately proxies cameras on the local LAN, so private ranges stay reachable. Reject only loopback, link-local and other reserved addresses via FILTER_FLAG_NO_RES_RANGE, which covers 127.0.0.0/8, ::1, fe80::/10 and 169.254.0.0/16 (cloud metadata) without excluding 10/8, 172.16/12, 192.168/16 or fc00::/7. Resolve the host first so a hostname cannot point at those ranges. Also require canEdit('Monitors') — the only consumer is the discovery thumbnail in add_monitors, which already demands that right — and stop reading $url_parts['user'] without an isset() guard. Refs GHSA-g28p-q36w-h3c5, GHSA-rq9f-p634-rrpg. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit b806de5e7633a17c2938ffb98f33100da9634488) --- web/views/image.php | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/web/views/image.php b/web/views/image.php index 7f3c8a51a..0bea69b32 100644 --- a/web/views/image.php +++ b/web/views/image.php @@ -53,6 +53,14 @@ if ( !function_exists('imagescale') ) { } if (!empty($_REQUEST['proxy'])) { + // The only consumer is the camera discovery thumbnail on add_monitors, which + // already requires monitor editing rights. Don't hand an outbound fetch + // primitive to plain event viewers. + if (!canEdit('Monitors')) { + ZM\Warning('Insufficient privileges to use the image proxy'); + return; + } + $url = $_REQUEST['proxy']; if (!$url) { ZM\Warning('No url passed to image proxy'); @@ -65,7 +73,42 @@ if (!empty($_REQUEST['proxy'])) { ZM\Warning('Image proxy only supports http/https URLs'); return; } - $username = $url_parts['user']; + + $host = isset($url_parts['host']) ? $url_parts['host'] : ''; + if ($host === '') { + ZM\Warning('Image proxy requires a host in the url'); + return; + } + + // Guard against SSRF. Discovery legitimately proxies cameras on the local + // LAN, so private ranges stay reachable, but loopback, link-local and other + // reserved addresses (127.0.0.1, ::1, 169.254.169.254 cloud metadata) are + // refused. FILTER_FLAG_NO_RES_RANGE covers exactly those without excluding + // 10/8, 172.16/12, 192.168/16 or fc00::/7. + if (filter_var($host, FILTER_VALIDATE_IP)) { + $addresses = array($host); + } else { + $addresses = gethostbynamel($host); + if (!$addresses) $addresses = array(); + $aaaa = @dns_get_record($host, DNS_AAAA); + if ($aaaa) { + foreach ($aaaa as $rr) { + if (!empty($rr['ipv6'])) $addresses[] = $rr['ipv6']; + } + } + } + if (!$addresses) { + ZM\Warning('Image proxy could not resolve '.$host); + return; + } + foreach ($addresses as $address) { + if (!filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)) { + ZM\Warning('Image proxy refusing reserved address '.$address.' for host '.$host); + return; + } + } + + $username = isset($url_parts['user']) ? $url_parts['user'] : ''; $password = isset($url_parts['pass']) ? $url_parts['pass'] : ''; $method = 'GET';