Files
zoneminder/tests/php/test_zms_socket_path.php
T
Nic BoetandClaude Opus 4.8 1d6dcff83b fix: name zms command sockets after the user the stream serves refs GHSA-7hhq-xw65-4crf
ajax/stream.php sends stream commands (query, pause, seek, zoom, quit)
to the zms whose unix socket matches a connkey, with no permission
check. Connkeys are six random digits chosen when the stream starts,
so any logged-in user who guessed or saw one could read the status of
another user's stream, of a monitor they may not view included, and
pause or end it. A second zms started with the same connkey also waited
on the first one's lock and then took over its socket.

zms already authenticates the user and checks their access to the
monitor or event before it streams. Keep that user's id and put it in
the lock, command socket and reply socket names
(zms-<connkey>-<user id>s.sock); stream.php builds the names from the
caller's own id through zmsSocketPath(), so a request reaches only the
caller's streams. Without authentication the id is 0 on both sides.

Adds a StreamBase test in tests/zm_stream.cpp and
tests/php/test_zms_socket_path.php, which pin the same names. The
existing command-socket-teardown test is updated for the owner-suffixed
names (it postdates the reporter's base commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-31 11:11:01 -04:00

47 lines
1.9 KiB
PHP

<?php
// Security regression test: ajax/stream.php must address a zms by the names zms
// itself uses, which carry the id of the user zms authenticated. Before, the
// names held only the 6-digit connkey, so any logged-in user who guessed one
// could query, pause or quit another user's stream, of a monitor they may not
// view included.
//
// tests/zm_stream.cpp checks the C++ side (StreamBase::openComms()) against the
// same names.
//
// zmsSocketPath() is taken from functions.php, which can't be loaded on its own.
//
// Run as: php tests/php/test_zms_socket_path.php
$failures = 0;
$passes = 0;
function check($name, $got, $want) {
global $failures, $passes;
if ($got === $want) {
$passes++;
echo "ok - $name\n";
} else {
$failures++;
echo "FAIL - $name (got ".var_export($got, true).", want ".var_export($want, true).")\n";
}
}
$source = file_get_contents(__DIR__.'/../../web/includes/functions.php');
if (!preg_match('/^function zmsSocketPath\(.*?^}/ms', $source, $m)) {
echo "FAIL - zmsSocketPath() not found in functions.php\n";
exit(1);
}
define('ZM_PATH_SOCKS', '/run/zm');
eval($m[0]);
check('command socket of user 7', zmsSocketPath('004242', 7, 's.sock'), '/run/zm/zms-004242-7s.sock');
check('reply socket of user 7', zmsSocketPath(4242, '7', 'w.sock'), '/run/zm/zms-004242-7w.sock');
check('lock of user 7', zmsSocketPath(4242, 7, '.lock'), '/run/zm/zms-004242-7.lock');
check('another user gets another socket', zmsSocketPath(4242, 8, 's.sock') !== zmsSocketPath(4242, 7, 's.sock'), true);
check('no authentication: owner 0', zmsSocketPath(4242, 0, 's.sock'), '/run/zm/zms-004242-0s.sock');
check('the connkey stays numeric', zmsSocketPath('42/../x', 7, 's.sock'), '/run/zm/zms-000042-7s.sock');
check('the owner stays numeric', zmsSocketPath(4242, '7/../x', 's.sock'), '/run/zm/zms-004242-7s.sock');
echo "\n$passes passed, $failures failed\n";
exit($failures ? 1 : 0);