mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-03 07:55:22 -04:00
ZM_AUTH_HASH_SECRET signs the JWT access and refresh tokens, and its default is a fixed string in the public source. Nothing generated a per-install value and tokens signed with the default verified normally, so on an install with auth on and the secret untouched anyone could sign an admin token and be accepted by the web UI, the API and zms. - ZoneMinder::Config::saveConfigToDB() now replaces an empty or default secret with 32 random bytes from /dev/urandom, hex encoded. Package installs and upgrades run zmupdate.pl -f, which saves the config, so existing installs get a secret on upgrade. A secret the admin set is left alone. - validateToken() in PHP and zmLoadTokenUser() in C++ refuse to verify tokens while the secret is empty or the default, and the API refuses to issue them, as it already did for an empty secret. - zmLoadTokenUser() no longer writes the signing key to the debug log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
338 lines
12 KiB
PHP
338 lines
12 KiB
PHP
<?php
|
|
App::uses('AppController', 'Controller');
|
|
|
|
class HostController extends AppController {
|
|
|
|
public $components = array('RequestHandler');
|
|
|
|
public function daemonCheck($daemon=false, $args=false) {
|
|
# To try to prevent abuse here, we are only going to allow certain characters in the daemon and args.
|
|
$count = 0;
|
|
$safe_daemon = $daemon ? preg_replace('/[^A-Za-z0-9\- \.]/', '', $daemon, -1, $count) : false;
|
|
if ($count) Error("Invalid characters found in daemon string ($daemon). Potential attack?");
|
|
$safe_args = $args ? preg_replace('/[^A-Za-z0-9\- \.]/', '', $args, -1, $count) : false;
|
|
if ($count) Error("Invalid characters found in args string ($args). Potential attack?");
|
|
|
|
$string = ZM_PATH_BIN.'/zmdc.pl check';
|
|
if ($safe_daemon) {
|
|
$string .= ' '.$safe_daemon;
|
|
if ($safe_args) $string .= ' '.$safe_args;
|
|
}
|
|
$result = exec($string);
|
|
$result = preg_match('/running/', $result);
|
|
|
|
$this->set(array(
|
|
'result' => $result,
|
|
'_serialize' => array('result')
|
|
));
|
|
}
|
|
|
|
// an interface to individually control the various ZM daemons
|
|
// invocation: https://server/zm/api/host/daemonControl/<daemon>.pl/<command>.json
|
|
// note that this API is only for interaction with a specific
|
|
// daemon. zmdc also allows other functions like logrot/etc
|
|
public function daemonControl($daemon, $command) {
|
|
global $user;
|
|
if ($command == 'check' || $command == 'status') {
|
|
$permission = 'View';
|
|
} else {
|
|
$permission = 'Edit';
|
|
}
|
|
$allowed = (!$user) || ($user->System() == $permission );
|
|
if ( !$allowed ) {
|
|
throw new UnauthorizedException(__("Insufficient privileges"));
|
|
return;
|
|
}
|
|
# To try to prevent abuse here, we are only going to allow certain characters in the daemon and args.
|
|
$safe_daemon = preg_replace('/[^A-Za-z0-9\-\.]/', '', $daemon, -1, $count);
|
|
if ($count) Error("Invalid characters found in daemon string ($daemon). Potential attack?");
|
|
$safe_command = preg_replace('/[^a-z]/', '', $command, -1, $count);
|
|
if ($count) Error("Invalid characters found in command string ($command). Potential attack?");
|
|
$string = ZM_PATH_BIN."/zmdc.pl $safe_command $safe_daemon";
|
|
$result = exec($string);
|
|
$this->set(array(
|
|
'result' => $result,
|
|
'_serialize' => array('result')
|
|
));
|
|
}
|
|
|
|
function getLoad() {
|
|
# getSysLoadHTML() in the web ui renders nothing without canView('System'),
|
|
# so the same figure is not handed out here to an account without it.
|
|
global $user;
|
|
if ($user and ($user->System() == 'None')) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
$load = sys_getloadavg();
|
|
|
|
$this->set(array(
|
|
'load' => $load,
|
|
'_serialize' => array('load')
|
|
));
|
|
}
|
|
|
|
function login() {
|
|
$ver = $this->_getVersion();
|
|
|
|
$login_array = [];
|
|
$login_array['version'] = $ver[0];
|
|
$login_array['apiversion'] = $ver[1];
|
|
|
|
if (!ZM_OPT_USE_AUTH) {
|
|
$login_array['_serialize'] = array_keys($login_array);
|
|
$this->set($login_array);
|
|
return;
|
|
}
|
|
|
|
$username = $this->request->query('user') ? $this->request->query('user') : $this->request->data('user');
|
|
if ( !$username )
|
|
$username = $this->request->query('username') ? $this->request->query('username') : $this->request->data('username');
|
|
$password = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass');
|
|
if ( !$password )
|
|
$password = $this->request->query('password') ? $this->request->query('password') : $this->request->data('password');
|
|
|
|
$token = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token');
|
|
|
|
if ( !($username && $password) && !$token ) {
|
|
throw new UnauthorizedException(__('No identity provided'));
|
|
}
|
|
|
|
$cred = [];
|
|
|
|
if ( $username && $password ) {
|
|
// Mint the token for the account that actually authenticated, never for
|
|
// a name taken from the request. beforeFilter() authenticates from
|
|
// user=/pass= (and zm_authenticate_request() from username=/password=),
|
|
// while the subject used to be read straight out of `user`. Because the
|
|
// two were never cross-checked, a caller could authenticate with their
|
|
// own low-privileged credentials and ask for a token issued to admin.
|
|
// See GHSA-m77q-66v7-j3fq.
|
|
global $user;
|
|
if ( !$user ) {
|
|
throw new UnauthorizedException(__('Not authenticated'));
|
|
}
|
|
ZM\Debug('Username and password provided, generating access and refresh tokens');
|
|
$cred = $this->_getCredentials(true, '', $user->Username()); // generate refresh
|
|
} else {
|
|
ZM\Debug('Only generating access token');
|
|
$cred = $this->_getCredentials(false, $token); // don't generate refresh
|
|
}
|
|
if ($cred) {
|
|
$login_array['access_token'] = $cred[0];
|
|
$login_array['access_token_expires'] = $cred[1];
|
|
|
|
if ( $username && $password ) {
|
|
$login_array['refresh_token'] = $cred[2];
|
|
$login_array['refresh_token_expires'] = $cred[3];
|
|
}
|
|
}
|
|
|
|
if ( ZM_OPT_USE_LEGACY_API_AUTH ) {
|
|
$cred_depr = $this->_getCredentialsDeprecated();
|
|
if ($cred_depr) {
|
|
$login_array['credentials'] = $cred_depr[0];
|
|
$login_array['append_password'] = $cred_depr[1];
|
|
}
|
|
} else {
|
|
ZM\Debug('Legacy Auth is disabled, not generating auth= credentials');
|
|
}
|
|
|
|
$login_array['_serialize'] = array_keys($login_array);
|
|
$this->set($login_array);
|
|
} // end function login()
|
|
|
|
// clears out session
|
|
function logout() {
|
|
userLogout();
|
|
|
|
$this->set(array(
|
|
'result' => 'ok',
|
|
'_serialize' => array('result')
|
|
));
|
|
|
|
} // end function logout()
|
|
|
|
private function _getCredentialsDeprecated() {
|
|
$credentials = '';
|
|
$appendPassword = 0;
|
|
if ( ZM_OPT_USE_AUTH ) {
|
|
require_once __DIR__ .'/../../../includes/auth.php';
|
|
if ( ZM_AUTH_RELAY == 'hashed' ) {
|
|
$credentials = 'auth='.generateAuthHash(ZM_AUTH_HASH_IPS, true);
|
|
} else {
|
|
$credentials = 'user='.$_SESSION['Username'].'&pass=';
|
|
$appendPassword = 1;
|
|
}
|
|
}
|
|
return array($credentials, $appendPassword);
|
|
}
|
|
|
|
private function _getCredentials($generate_refresh_token=false, $token='', $username='') {
|
|
if (!ZM_OPT_USE_AUTH) {
|
|
ZM\Debug('OPT_USE_AUTH is turned off. Tokens will be null');
|
|
return;
|
|
}
|
|
|
|
require_once __DIR__ .'/../../../includes/auth.php';
|
|
if (!authHashSecretIsSet())
|
|
throw new ForbiddenException(__('Please create a valid AUTH_HASH_SECRET in ZoneMinder'));
|
|
|
|
require_once __DIR__.'/../../../vendor/autoload.php';
|
|
|
|
if ($token) {
|
|
// If we have a token, we need to derive username from there
|
|
$ret = validateToken($token, 'refresh', true);
|
|
$username = $ret[0]->Username();
|
|
}
|
|
|
|
ZM\Debug("Creating token for \"$username\"");
|
|
|
|
/* we won't support AUTH_HASH_IPS in token mode
|
|
reasons:
|
|
a) counter-intuitive for mobile consumers
|
|
b) zmu will never be able to to validate via a token if we sign
|
|
it after appending REMOTE_ADDR
|
|
|
|
if (ZM_AUTH_HASH_IPS) {
|
|
$key = $key . $_SERVER['REMOTE_ADDR'];
|
|
}*/
|
|
|
|
$access_issued_at = time();
|
|
$access_ttl = max(ZM_AUTH_HASH_TTL, 1) * 3600;
|
|
|
|
// by default access token will expire in 2 hrs
|
|
// you can change it by changing the value of ZM_AUTH_HASH_TTL
|
|
$access_expire_at = $access_issued_at + $access_ttl;
|
|
|
|
$access_token = array(
|
|
'iss' => 'ZoneMinder',
|
|
'iat' => $access_issued_at,
|
|
'exp' => $access_expire_at,
|
|
'user' => $username,
|
|
'type' => 'access'
|
|
);
|
|
|
|
$jwt_access_token = \Firebase\JWT\JWT::encode($access_token, ZM_AUTH_HASH_SECRET, 'HS256');
|
|
|
|
$jwt_refresh_token = '';
|
|
$refresh_ttl = 0;
|
|
|
|
if ($generate_refresh_token) {
|
|
$refresh_issued_at = time();
|
|
$refresh_ttl = 24 * 3600; // 1 day
|
|
|
|
$refresh_expire_at = $refresh_issued_at + $refresh_ttl;
|
|
$refresh_token = array(
|
|
'iss' => 'ZoneMinder',
|
|
'iat' => $refresh_issued_at,
|
|
'exp' => $refresh_expire_at,
|
|
'user' => $username,
|
|
'type' => 'refresh'
|
|
);
|
|
$jwt_refresh_token = \Firebase\JWT\JWT::encode($refresh_token, ZM_AUTH_HASH_SECRET, 'HS256');
|
|
} # end if generate_refresh_token
|
|
return array($jwt_access_token, $access_ttl, $jwt_refresh_token, $refresh_ttl);
|
|
} # end function _getCredentials($generate_refresh_token=false, $token='')
|
|
|
|
// If $mid is set, only return disk usage for that monitor
|
|
// Else, return an array of total disk usage, and per-monitor
|
|
// usage.
|
|
// This function is deprecated. Use the Storage object or monitor object instead
|
|
function getDiskPercent($mid = null) {
|
|
$this->loadModel('Monitor');
|
|
|
|
// If $mid is passed, see if it is valid
|
|
if ( $mid and !$this->Monitor->exists($mid) ) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
# Usage is reported per monitor, keyed by monitor name, so an account that
|
|
# cannot view a monitor was being told that it exists and how much disk it
|
|
# uses. Report only on the monitors it may see.
|
|
if ($mid) {
|
|
$one = $this->Monitor->find('first', array('conditions' => array('Id' => $mid)));
|
|
$zm_monitor = new ZM\Monitor($one['Monitor']);
|
|
if (!$zm_monitor->canView()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
}
|
|
|
|
$zm_dir_events = ZM_DIR_EVENTS;
|
|
|
|
// Test to see if $zm_dir_events is relative or absolute
|
|
#if ('/' === "" || strrpos($zm_dir_events, '/', -strlen($zm_dir_events)) !== TRUE) {
|
|
if ( substr($zm_dir_events, 0, 1) != '/' ) {
|
|
// relative - so add the full path
|
|
$zm_dir_events = ZM_PATH_WEB . '/' . $zm_dir_events;
|
|
}
|
|
|
|
if ( $mid ) {
|
|
// Get disk usage for $mid
|
|
ZM\Debug("Executing du -s0 $zm_dir_events/$mid | awk '{print \$1}'");
|
|
$usage = shell_exec("du -s0 ".escapeshellarg($zm_dir_events.'/'.$mid)." | awk '{print \$1}'");
|
|
} else {
|
|
$monitors = $this->Monitor->find('all', array(
|
|
'fields' => array('Id', 'Name', 'WebColour')
|
|
));
|
|
$usage = array();
|
|
|
|
// Add each monitor's usage to array
|
|
foreach ($monitors as $key => $value) {
|
|
$zm_monitor = new ZM\Monitor($value['Monitor']);
|
|
if (!$zm_monitor->canView()) continue;
|
|
$id = $value['Monitor']['Id'];
|
|
$name = $value['Monitor']['Name'];
|
|
$color = $value['Monitor']['WebColour'];
|
|
|
|
$space = shell_exec("du -s0 $zm_dir_events/$id | awk '{print $1}'");
|
|
if ( $space == null ) {
|
|
$space = 0;
|
|
}
|
|
$space = $space/1024/1024;
|
|
|
|
$usage[$name] = array(
|
|
'space' => rtrim($space),
|
|
'color' => $color
|
|
);
|
|
}
|
|
|
|
// Add total usage to array
|
|
$space = shell_exec( "df $zm_dir_events |tail -n1 | awk '{print $3 }'");
|
|
$space = $space/1024/1024;
|
|
$usage['Total'] = array(
|
|
'space' => rtrim($space),
|
|
'color' => '#F7464A'
|
|
);
|
|
}
|
|
|
|
$this->set(array(
|
|
'usage' => $usage,
|
|
'_serialize' => array('usage')
|
|
));
|
|
}
|
|
|
|
function getTimeZone() {
|
|
//http://php.net/manual/en/function.date-default-timezone-get.php
|
|
$tz = date_default_timezone_get();
|
|
$this->set(array(
|
|
'tz' => $tz,
|
|
'_serialize' => array('tz')
|
|
));
|
|
}
|
|
|
|
private function _getVersion() {
|
|
$version = ZM_VERSION;
|
|
$apiversion = '2.0';
|
|
return array($version, $apiversion);
|
|
}
|
|
|
|
function getVersion() {
|
|
$val = $this->_getVersion();
|
|
$this->set(array(
|
|
'version' => $val[0],
|
|
'apiversion' => $val[1],
|
|
'_serialize' => array('version', 'apiversion')
|
|
));
|
|
}
|
|
}
|