fix: restrict image proxy to non-reserved addresses and monitor editors

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 <img> 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) <noreply@anthropic.com>
(cherry picked from commit b806de5e76)
This commit is contained in:
Isaac Connor
2026-07-19 11:45:54 -04:00
parent bbb03d2d05
commit 08ca9d97ba

View File

@@ -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';