mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-09-13 13:49:22 -04:00
auth.php ended in a 130-line block at file scope, so merely including the file authenticated the current request: it read $_REQUEST, opened a session, queried the database, and on a login could rewrite the user's stored password hash and populate $_SESSION. Any caller that wanted one of the functions in the file got all of that as a side effect, and the order of includes decided when it ran. HostController requires auth.php twice purely to reach generateAuthHash() and validateToken(). Move the block into zm_authenticate_request() and call it explicitly from the two places that want it, web/index.php and AppController::beforeFilter(). The function returns the ZM\User or null and still sets the global $user, so the views, ajax handlers and API controllers that read that global are unaffected. HostController now gets only the function definitions from its requires, which is all it ever wanted. Inside a function the five `unset($user)` calls would drop the local binding and leave the global set, so they become `$user = null` - the idiom the rest of the file already uses for this, and one that keeps isset($user) false for the gate at index.php:255. The block's other locals ($ret, $username, $password, $sql) no longer leak into the caller's scope, which in beforeFilter() means they can no longer collide with the variables of the same name it assigns just after. The body is otherwise unchanged; `git diff -w` shows only the wrapper, those five assignments and the return. Tests: tests/php/test_auth_no_include_side_effects.php tokenises auth.php and asserts nothing executes at file scope, with a fixture check so a broken detector cannot pass vacuously. 4 assertions, all pass. Verified it reports the pre-refactor file's file-scope block, so it would have caught this. Not covered by tests: the login, logout, auth-hash and API token flows this touches. auth.php cannot be included without a database (User.php pulls in database.php, which connects at include time), so the check is structural. Needs manual testing on an installed tree before merging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01477mR97vfnK6zczbHgzq6T
193 lines
7.5 KiB
PHP
193 lines
7.5 KiB
PHP
<?php
|
|
/**
|
|
* Application level Controller
|
|
*
|
|
* This file is application-wide controller file. You can put all
|
|
* application-wide controller-related methods here.
|
|
*
|
|
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
|
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
|
*
|
|
* Licensed under The MIT License
|
|
* For full copyright and license information, please see the LICENSE.txt
|
|
* Redistributions of files must retain the above copyright notice.
|
|
*
|
|
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
|
* @link https://cakephp.org CakePHP(tm) Project
|
|
* @package app.Controller
|
|
* @since CakePHP(tm) v 0.2.9
|
|
* @license https://www.opensource.org/licenses/mit-license.php MIT License
|
|
*/
|
|
App::uses('Controller', 'Controller');
|
|
App::uses('CrudControllerTrait', 'Crud.Lib');
|
|
|
|
/**
|
|
* Application Controller
|
|
*
|
|
* Add your application-wide methods in the class below, your controllers
|
|
* will inherit them.
|
|
*
|
|
* @package app.Controller
|
|
* @link https://book.cakephp.org/2.0/en/controllers.html#the-app-controller
|
|
*/
|
|
class AppController extends Controller {
|
|
use CrudControllerTrait;
|
|
|
|
public $components = [
|
|
'RequestHandler',
|
|
'Crud.Crud' => [
|
|
'actions' => [
|
|
'index' => 'Crud.Index',
|
|
'add' => 'Crud.Add',
|
|
'edit' => 'Crud.Edit',
|
|
'view' => 'Crud.View',
|
|
'keyvalue' => 'Crud.List',
|
|
'category' => 'Crud.Category'
|
|
],
|
|
'listeners' => ['Api', 'ApiTransformation']
|
|
#],
|
|
#'DebugKit.Toolbar' => [
|
|
# 'bootstrap' => true, 'routes' => true
|
|
]
|
|
];
|
|
|
|
// Global beforeFilter function
|
|
//Zoneminder sets the username session variable
|
|
// to the logged in user. If this variable is set
|
|
// then you are logged in
|
|
// its pretty simple to extend this to also check
|
|
// for role and deny API access in future
|
|
// Also checking to do this only if ZM_OPT_USE_AUTH is on
|
|
public function beforeFilter() {
|
|
if ( ! ZM_OPT_USE_API ) {
|
|
throw new UnauthorizedException(__('API Disabled'));
|
|
return;
|
|
}
|
|
|
|
# For use throughout the app. If not logged in, this will be null.
|
|
global $user;
|
|
require_once __DIR__ .'/../../../includes/auth.php';
|
|
# This will auto-login if username=&password= are set, or auth=
|
|
zm_authenticate_request();
|
|
|
|
if ( ZM_OPT_USE_AUTH ) {
|
|
if ( ZM_OPT_USE_LEGACY_API_AUTH or !strcasecmp($this->params->action, 'login') ) {
|
|
# This is here because historically we allowed user=&pass= in the api. web-ui auth uses username=&password=
|
|
$username = $this->request->query('user') ? $this->request->query('user') : $this->request->data('user');
|
|
$password = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass');
|
|
if ( $username and $password ) {
|
|
$ret = validateUser($username, $password);
|
|
$user = $ret[0];
|
|
$retstatus = $ret[1];
|
|
if ( !$user ) {
|
|
throw new UnauthorizedException(__($retstatus));
|
|
return;
|
|
}
|
|
ZM\Debug("Login successful for user \"$username\"");
|
|
}
|
|
}
|
|
|
|
if ( ZM_OPT_USE_LEGACY_API_AUTH ) {
|
|
require_once __DIR__ .'/../../../includes/session.php';
|
|
$stateful = $this->request->query('stateful') ? $this->request->query('stateful') : $this->request->data('stateful');
|
|
if ( $stateful ) {
|
|
// zm_session_start() already populates $_SESSION['remoteAddr'] from
|
|
// HTTP_X_FORWARDED_FOR (falling back to REMOTE_ADDR), matching what
|
|
// getAuthUser() uses for validation. Don't overwrite it with bare
|
|
// REMOTE_ADDR here — that bound the hash to the proxy IP and broke
|
|
// validation behind a reverse proxy.
|
|
zm_session_start();
|
|
if ($user) {
|
|
$_SESSION['username'] = $user->Username();
|
|
if ( ZM_AUTH_RELAY == 'plain' ) {
|
|
// Need to save this in session, can't use the value in User because it is hashed
|
|
$_SESSION['password'] = $_REQUEST['password'];
|
|
}
|
|
generateAuthHash(ZM_AUTH_HASH_IPS);
|
|
}
|
|
session_write_close();
|
|
} else if ( isset($_COOKIE['ZMSESSID']) and !$user ) {
|
|
# Have a cookie set, try to load user by session
|
|
if ( ! is_session_started() )
|
|
zm_session_start();
|
|
|
|
ZM\Debug(print_r($_SESSION, true));
|
|
$user = userFromSession();
|
|
session_write_close();
|
|
}
|
|
}
|
|
|
|
# NON LEGACY, token based access
|
|
$token = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token');
|
|
if ( $token ) {
|
|
// if you pass a token to login, we should only allow
|
|
// refresh tokens to regenerate new access and refresh tokens
|
|
if ( !strcasecmp($this->params->action, 'login') ) {
|
|
$only_allow_token_type = 'refresh';
|
|
} else {
|
|
// for any other methods, don't allow refresh tokens
|
|
// they are supposed to be infrequently used for security
|
|
// purposes
|
|
$only_allow_token_type = 'access';
|
|
}
|
|
$ret = validateToken($token, $only_allow_token_type, true);
|
|
$user = $ret[0];
|
|
$retstatus = $ret[1];
|
|
if ( !$user ) {
|
|
throw new UnauthorizedException(__($retstatus));
|
|
return;
|
|
}
|
|
} # end if token
|
|
|
|
if ( $user and ( $user->APIEnabled() != 1 ) ) {
|
|
ZM\Error('API disabled for: '.$user->Username());
|
|
throw new UnauthorizedException(__('API disabled for: '.$user->Username()));
|
|
$user = null;
|
|
}
|
|
|
|
// We need to reject methods that are not authenticated
|
|
// besides login and logout
|
|
if ( strcasecmp($this->params->action, 'logout') ) {
|
|
if ( !( $user and $user->Username() ) ) {
|
|
throw new UnauthorizedException(__('Not Authenticated'));
|
|
return;
|
|
} else if ( !( $user and $user->Enabled() ) ) {
|
|
throw new UnauthorizedException(__('User is not enabled'));
|
|
return;
|
|
}
|
|
} # end if ! login or logout
|
|
|
|
} # end if ZM_OPT_AUTH
|
|
// make sure populated user object has APIs enabled
|
|
|
|
if (isset($_SERVER['HTTP_ORIGIN'])) {
|
|
global $Servers;
|
|
if ( sizeof($Servers) < 1 ) {
|
|
# Only need CORSHeaders in the event that there are multiple servers in use.
|
|
# ICON: Might not be true. multi-port?
|
|
if ( ZM_MIN_STREAMING_PORT ) {
|
|
ZM\Debug('Setting default Access-Control-Allow-Origin from ' . $_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Credentials: true');
|
|
$this->response->header('Access-Control-Allow-Headers: x-requested-with,x-request');
|
|
}
|
|
return;
|
|
}
|
|
foreach ($Servers as $Server) {
|
|
if (
|
|
preg_match('/^(https?:\/\/)?'.preg_quote($Server->Hostname(),'/').'/i', $_SERVER['HTTP_ORIGIN'])
|
|
or
|
|
preg_match('/^(https?:\/\/)?'.preg_quote($Server->Name(),'/').'/i', $_SERVER['HTTP_ORIGIN'])
|
|
) {
|
|
ZM\Debug('Setting Access-Control-Allow-Origin from '.$_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Credentials: true');
|
|
$this->response->header('Access-Control-Allow-Headers: x-requested-with,x-request');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
} # end function beforeFilter()
|
|
}
|