mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-06-23 13:09:23 -04:00
image.php, view_video.php and view_hls.php previously checked only the
coarse canView('Events') / canView('Snapshots') role before streaming
media for a user-supplied event id. An authenticated user denied access
to a monitor could still fetch event snapshots, captured frames,
recorded MP4s and HLS manifests for events belonging to that monitor by
calling the direct endpoints with the event id.
Call Event->canView() after loading the event and return 404 on denial
so the event id cannot be enumerated. view_video also validates
Event->Id() so unknown ids return 404 instead of an empty 200 body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
2.6 KiB
PHP
89 lines
2.6 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($_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 404 Not Found');
|
|
die('HLS manifest not available for this event');
|
|
}
|
|
|
|
// Don't serve an m3u8 with no fragments — the event may have just started
|
|
$m3u8_content_check = file_get_contents($m3u8_path);
|
|
if (strpos($m3u8_content_check, '#EXTINF:') === false) {
|
|
header('HTTP/1.1 404 Not Found');
|
|
die('HLS manifest has no fragments yet');
|
|
}
|
|
|
|
// 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;
|