Files
zoneminder/web/views/view_hls.php
T
Isaac ConnorandClaude Opus 5.5 c3d2e542bc fix: never build an object from an id the request sent as an array
ZM_Object's constructor treats an array as the object's row, and validInt()
and validCardinal() returned an array as they got it (preg_replace() does).
So wherever a request id reached `new ZM\X(...)`, raw or through those
validators, a crafted array built an object whose Id, UserId or MonitorId
the sender chose. The permission check then looked at those, and the delete
or update that followed used the Id. A user with Events=View deleted another
user's filter by posting Id[Id]=<filter>&Id[UserId]=<self> to the filter
delete action; the same shape reached events (MonitorId), users, reports,
snapshots and storage.

validInt() and validCardinal() now give '' for anything that isn't a single
value. The places that built an object from a raw $_REQUEST value (the
control and event ajax handlers, the event, filter, report, snapshot and
storage actions, view_hls, and the download and event detail modals) take
the id through validCardinal().

tests/php/test_request_ids_scalar.php covers the validators with arrays and
fails if web code builds an object straight from $_REQUEST, $_GET or $_POST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:07:08 -04:00

96 lines
2.8 KiB
PHP

<?php
//
// ZoneMinder HLS manifest server
// Serves the pre-built m3u8 manifest for an event's byte-range HLS playback.
//
if (!canView('Events')) {
$view = 'error';
return;
}
ob_end_clean();
require_once('includes/Event.php');
if (empty($_REQUEST['eid'])) {
header('HTTP/1.1 400 Bad Request');
die('Missing eid parameter');
}
$Event = new ZM\Event(validCardinal($_REQUEST['eid']));
if (!$Event->Id()) {
header('HTTP/1.1 404 Not Found');
die('Event not found');
}
// Per-event ACL: coarse canView('Events') isn't enough — the user may be denied
// access to the monitor that owns this event (GHSA-vj5r-pc2v-gfwv). Return the
// same 404 as a missing event so the id isn't leaked.
if (!$Event->canView()) {
ZM\Warning('Event '.$_REQUEST['eid'].' HLS access denied');
header('HTTP/1.1 404 Not Found');
die('Event not found');
}
$m3u8_path = $Event->Path() . '/index.m3u8';
if (!file_exists($m3u8_path)) {
header('HTTP/1.1 204 No Content');
header('Cache-Control: no-cache');
exit;
}
$m3u8_content_check = file_get_contents($m3u8_path);
if ($m3u8_content_check === false) {
header('HTTP/1.1 204 No Content');
header('Cache-Control: no-cache');
ZM\Debug('HLS manifest ' . $m3u8_path . ' is not available yet');
exit;
} else if (strpos($m3u8_content_check, '#EXTINF:') === false) {
header('HTTP/1.1 204 No Content');
header('Cache-Control: no-cache');
ZM\Debug('HLS manifest ' . $m3u8_path .' has no fragments yet');
exit;
}
// Build auth query string for segment URLs
$auth_query = '';
if (ZM_OPT_USE_AUTH) {
if (ZM_AUTH_RELAY == 'hashed') {
$auth_query = '&auth=' . generateAuthHash(ZM_AUTH_HASH_IPS);
} else if (ZM_AUTH_RELAY == 'plain') {
$auth_query = '&user=' . $_SESSION['username'] . '&pass=' . $_SESSION['password'];
} else if (ZM_AUTH_RELAY == 'none') {
$auth_query = '&user=' . $_SESSION['username'];
}
}
// Read the m3u8 and inject auth tokens into segment URLs
$content = file_get_contents($m3u8_path);
// The m3u8 has relative URLs like "index.php?view=view_video&eid=123"
// We need to make them absolute with the server path and add auth
$Server = $Event->Server();
$base_url = $Server->PathToIndex();
// Replace bare relative segment URLs with full paths including auth.
// The m3u8 has lines like "index.php?view=view_video&eid=N&file=F" — capture
// only the query string (after "index.php?") so the replacement doesn't emit
// "/zm/index.php?index.php?view=…".
$content = preg_replace(
'/^index\.php\?(.+)$/m',
$base_url . '?$1' . $auth_query,
$content
);
// Also fix the EXT-X-MAP URI (initialization segment) the same way.
$content = preg_replace(
'/URI="index\.php\?([^"]+)"/m',
'URI="' . $base_url . '?$1' . $auth_query . '"',
$content
);
header('Content-Type: application/vnd.apple.mpegurl');
header('Cache-Control: no-cache');
echo $content;
exit;