Observium_CE/html/includes/sessions.inc.php

611 lines
22 KiB
PHP

<?php
/**
* Observium
*
* This file is part of Observium.
*
* @package observium
* @subpackage web
* @copyright (C) 2006-2013 Adam Armstrong, (C) 2013-2023 Observium Limited
*
*/
// Sessions
function session_allow_cidr() {
global $config;
if (safe_count($config['web_session_cidr'])) {
return match_network(get_remote_addr($config['web_session_ip_by_header']), $config['web_session_cidr']);
}
return TRUE;
}
function session_remote_address() {
// Store logged remote IP with real proxied IP (if configured and available)
$remote_addr = get_remote_addr();
$remote_addr_header = get_remote_addr(TRUE); // Remote addr by http header
if ($remote_addr_header && $remote_addr !== $remote_addr_header) {
return $remote_addr_header . ' (' . $remote_addr . ')';
}
return $remote_addr;
}
function session_set_debug($debug_web_requested = FALSE) {
if (!defined('OBS_DEBUG')) // OBS_DEBUG not defined by default for unprivileged users in definitions
{
if ($_SESSION['userlevel'] < 7 || !$debug_web_requested) // Note, use $config['web_debug_unprivileged'] = TRUE;
{
// DO NOT ALLOW show debug output for users with privilege level less than "Global Secure Read"
define('OBS_DEBUG', 0);
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
//ini_set('error_reporting', 0); // Use default php config
} else {
define('OBS_DEBUG', 1);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
//ini_set('error_reporting', E_ALL ^ E_NOTICE);
ini_set('error_reporting', E_ALL ^ E_NOTICE ^ E_WARNING);
}
}
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
// ini_set('error_reporting', E_ALL ^ E_NOTICE);
}
// DOCME needs phpdoc block
function session_logout($relogin = FALSE, $message = NULL)
{
global $debug_auth;
// Save auth failure message for later re-use
$auth_message = $_SESSION['auth_message'];
if ($_SESSION['authenticated']) {
$auth_log = 'Logged Out';
} else {
$auth_log = 'Authentication Failure';
}
if ($message) {
$auth_log .= ' (' . $message . ')';
}
if ($debug_auth) {
$debug_log = $GLOBALS['config']['log_dir'] . '/debug_logout_' . date("Y-m-d_H:i:s") . '.log';
file_put_contents($debug_log, var_export($_SERVER, TRUE), FILE_APPEND);
file_put_contents($debug_log, var_export($_SESSION, TRUE), FILE_APPEND);
file_put_contents($debug_log, var_export($_COOKIE, TRUE), FILE_APPEND);
}
dbInsert(['user' => $_SESSION['username'],
'address' => session_remote_address(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'result' => $auth_log], 'authlog');
if (isset($_COOKIE['ckey'])) {
dbDelete('users_ckeys', "`username` = ? AND `user_ckey` = ?", [$_SESSION['username'], $_COOKIE['ckey']]);
} // Remove old ckeys from DB
// Unset cookies
$cookie_params = session_get_cookie_params();
$past = time() - 3600;
foreach ($_COOKIE as $cookie => $value) {
if (empty($cookie_params['domain'])) {
setcookie($cookie, '', $past, $cookie_params['path']);
} else {
setcookie($cookie, '', $past, $cookie_params['path'], $cookie_params['domain']);
}
}
unset($_COOKIE);
// Clean cache if possible
$cache_tags = ['__anonymous'];
if ($_SESSION['authenticated']) {
$cache_tags = ['__username=' . $_SESSION['username']];
}
del_cache_items($cache_tags);
// Unset session
@session_start();
if ($relogin) {
// Reset session and relogin (for example: HTTP auth)
$_SESSION['relogin'] = TRUE;
unset($_SESSION['authenticated'],
$_SESSION['user_id'],
$_SESSION['username'],
$_SESSION['user_encpass'], $_SESSION['password'],
$_SESSION['userlevel']);
session_write_close();
session_regenerate_id(TRUE);
} else {
// Kill current session, as authentication failed
unset($_SESSION);
session_unset();
session_destroy();
session_write_close();
//setcookie(session_name(),'',0,'/');
session_regenerate_id(TRUE);
// Re-set auth failure message for use on login page
//session_start();
$_SESSION['auth_message'] = $message;
}
}
/**
* Safe session_regenerate_id that doesn't loose session
*
* Code borrowed from https://www.php.net/manual/en/function.session-regenerate-id.php
*/
function safe_session_regenerate_id() {
global $debug_auth;
// New session ID is required to set proper session ID
// when session ID is not set due to unstable network.
$new_session_id = session_create_id();
if ($debug_auth) {
logfile('debug_auth.log', __LINE__ . " Session ID regenerated. IP=[" . get_remote_addr($GLOBALS['config']['web_session_ip_by_header']) .
"]. ID=[$new_session_id]." . web_debug_log_message());
}
$_SESSION['new_session_id'] = $new_session_id;
// Set destroy timestamp
$_SESSION['destroyed'] = time();
// Write and close current session;
session_write_close();
$tmp = $_SESSION; // Somehow the SESSION data is not kept
// Start session with new session ID
session_id($new_session_id);
ini_set('session.use_strict_mode', 0);
session_start();
ini_set('session.use_strict_mode', 1);
$_SESSION = $tmp;
// New session does not need them
unset($_SESSION['destroyed'], $_SESSION['new_session_id']);
}
/**
* Regenerate session ID for prevent attacks session hijacking and session fixation.
* Note, use this function after session_start() and before next session_commit()!
*
* Code borrowed from https://www.php.net/manual/en/function.session-regenerate-id.php
*
* @param int $lifetime_id Time in seconds for next regenerate session ID (default 30 min)
*/
function session_regenerate($lifetime_id = 1800)
{
session_start();
if (isset($_SESSION['destroyed'])) {
// logfile('debug_auth.log', __LINE__ . ' session destroyed ' . json_encode($_SESSION));
if ($_SESSION['destroyed'] < time() - 300) {
// logfile('debug_auth.log', __LINE__ . ' < 300 - logout');
// Should not happen usually. This could be attack or due to unstable network.
// Remove all authentication status of this users session.
session_logout(TRUE, 'Session destroyed');
return;
}
if (isset($_SESSION['new_session_id'])) {
// logfile('debug_auth.log', __LINE__ . ' got new_session_id: ' . $_SESSION['new_session_id']);
// Not fully expired yet. Could be lost cookie by unstable network.
// Try again to set proper session ID cookie.
// NOTE: Do not try to set session ID again if you would like to remove
// authentication flag.
session_write_close();
session_id($_SESSION['new_session_id']);
// New session ID should exist
session_start();
return;
}
}
$currenttime = time();
if ($lifetime_id != 1800 && is_numeric($lifetime_id) && $lifetime_id >= 300) {
$lifetime_id = (int)$lifetime_id;
} else {
$lifetime_id = 1800;
}
if (isset($_SESSION['starttime'])) {
// logfile('debug_auth.log', __LINE__ . ' ' . json_encode([$_SESSION['starttime'], $currenttime, $currenttime - $_SESSION['starttime']]));
if (($currenttime - $_SESSION['starttime']) >= $lifetime_id &&
!is_graph() && !is_ajax()) // Skip regenerate in graphs and in ajax
{
// logfile('debug_auth.log', __LINE__ . ' ' . 'session_regenerate() called at '.$currenttime);
// ID Lifetime expired, regenerate
safe_session_regenerate_id();
// Clean cache from _SESSION first, this cache used in ajax calls
if (isset($_SESSION['cache'])) {
unset($_SESSION['cache']);
}
$_SESSION['starttime'] = $currenttime;
}
} else {
$_SESSION['starttime'] = $currenttime;
}
}
/**
* Store encrypted password in $_SESSION['user_encpass'], required for some auth mechanism, ie ldap
*
* @param string $auth_password Plain password
* @param string $key Key for password encrypt
*
* @return string Encrypted password
*/
function session_encrypt_password($auth_password, $key)
{
// Store encrypted password
if ($GLOBALS['config']['auth_mechanism'] === 'ldap' &&
!($GLOBALS['config']['auth_ldap_bindanonymous'] || !safe_empty($GLOBALS['config']['auth_ldap_binddn'] . $GLOBALS['config']['auth_ldap_bindpw']))) {
if (OBS_ENCRYPT) {
if (OBS_ENCRYPT_MODULE === 'mcrypt') {
$key .= get_unique_id();
}
// For some admin LDAP functions required store encrypted password in session (userslist)
session_set_var('user_encpass', encrypt($auth_password, $key));
} else {
//session_set_var('user_encpass', base64_encode($auth_password));
session_set_var('encrypt_required', 1);
}
}
return $_SESSION['user_encpass'];
}
// DOCME needs phpdoc block
function session_is_active()
{
if (!is_cli()) {
// logfile('debug_auth.log', __LINE__ . " CONSTANT? ".var_export(session_status(), TRUE)." vs ".var_export(PHP_SESSION_ACTIVE, TRUE));
return session_status() === PHP_SESSION_ACTIVE;
}
// logfile('debug_auth.log', __LINE__ . " CLI?");
return FALSE;
}
/**
* Generate unique id for current user/browser, based on some unique params
*
* @return string
*/
function session_unique_id()
{
global $config, $debug_auth;
/* WiP. New User-Agent
if ($debug_auth && isset($_SERVER['HTTP_SEC_CH_UA'])) {
//print_message($_SERVER['HTTP_SEC_CH_UA']);
//print_message($_SERVER['HTTP_SEC_CH_UA_MOBILE']);
//r($_SERVER);
}
*/
$id = $_SERVER['HTTP_USER_AGENT']; // User agent
//$id .= $_SERVER['HTTP_ACCEPT']; // Browser accept headers // WTF, this header different for main and js/ajax queries
// Less entropy than HTTP_ACCEPT, but stable!
$id .= $_SERVER['HTTP_ACCEPT_ENCODING'];
$id .= $_SERVER['HTTP_ACCEPT_LANGUAGE'];
if ($config['web_session_ip']) {
$remote_ip = get_remote_addr($config['web_session_ip_by_header']); // Remote address by header if configured
$config['web_session_ipv6_prefix'] = ltrim($config['web_session_ipv6_prefix'], '/');
if (is_numeric($config['web_session_ipv6_prefix']) &&
$config['web_session_ipv6_prefix'] >= 1 && $config['web_session_ipv6_prefix'] <= 127 &&
get_ip_version($remote_ip) === 6) {
$remote_addr = Net_IPv6 ::getNetmask($remote_ip, (int)$config['web_session_ipv6_prefix']);
} else {
$remote_addr = $remote_ip;
}
$id .= $remote_addr; // User IP address
// Force reauth if remote IP changed
if ($_SESSION['authenticated']) {
if (isset($_SESSION['PREV_REMOTE_ADDR']) && $remote_addr !== $_SESSION['PREV_REMOTE_ADDR']) {
if ($debug_auth) {
logfile('debug_auth.log', __LINE__ . " IP changed. IP=[$remote_ip]. ID=[$id]." . web_debug_log_message());
logfile('debug_auth.log', __LINE__ . ' ' . var_export($_SESSION, TRUE));
}
// FIXME. Hrm, I forgot why not just session_logout()
//session_logout();
unset($_SESSION['authenticated'],
$_SESSION['user_id'],
$_SESSION['username'],
$_SESSION['user_encpass'], $_SESSION['password'],
$_SESSION['userlevel'],
$_SESSION['PREV_REMOTE_ADDR']);
reauth_with_message('Remote IP has changed.');
}
session_set_var('PREV_REMOTE_ADDR', $remote_addr); // Store current remote IP
}
}
// Force reauth if user-agent or login changed
if ($_SESSION['authenticated']) {
// Check and validate if User agent not changed
if (session_useragent_changed()) {
if ($debug_auth) {
logfile('debug_auth.log', __LINE__ . " UA changed. IP=[" . get_remote_addr($config['web_session_ip_by_header']) . "]. ID=[$id]." . web_debug_log_message());
logfile('debug_auth.log', __LINE__ . ' ' . var_export($_SESSION, TRUE));
}
session_logout();
reauth_with_message('Browser has changed.');
}
// Ie in API request can force different user
if (session_user_changed()) {
if ($debug_auth) {
logfile('debug_auth.log', __LINE__ . " Username changed. IP=[" . get_remote_addr($config['web_session_ip_by_header']) . "]. ID=[$id]." . web_debug_log_message());
logfile('debug_auth.log', __LINE__ . ' ' . var_export($_SESSION, TRUE));
}
session_logout();
reauth_with_message('User login has changed.');
}
//if ($debug_auth)
//{
// logfile('debug_auth.log', __LINE__ . ' ' . "IP=[".get_remote_addr($config['web_session_ip_by_header'])."]. ID=[$id]. URL=[".$_SERVER['REQUEST_URI']."]");
//}
}
$user_unique_id = md5($id);
// Next required JS cals:
// resolution = screen.width+"x"+screen.height+"x"+screen.colorDepth;
// timezone = new Date().getTimezoneOffset();
if (FALSE && $debug_auth) {
$debug_log_array = [$user_unique_id, $remote_addr, $_SERVER['HTTP_USER_AGENT'],
$_SERVER['HTTP_ACCEPT_ENCODING'], $_SERVER['HTTP_ACCEPT_LANGUAGE'],
$_COOKIE['OBSID']];
logfile('debug_auth.log', __LINE__ . ' ' . json_encode($debug_log_array));
}
//print_vars($id);
return $user_unique_id;
}
function session_useragent_changed() {
$ua = md5($_SERVER['HTTP_USER_AGENT']);
if (!isset($_SESSION['ua'])) {
session_set_var('ua', $ua);
return FALSE;
}
return $_SESSION['ua'] !== $ua;
}
function session_user_changed() {
if (isset($_GET['username'], $_GET['password']) &&
$_GET['username'] !== $_SESSION['username']) {
// GET Auth
return TRUE;
}
if (isset($_POST['username'], $_POST['password']) &&
$_POST['username'] !== $_SESSION['username']) {
// POST Auth
return TRUE;
}
if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) &&
$_SERVER['PHP_AUTH_USER'] !== $_SESSION['username']) {
// Basic Auth
return TRUE;
}
return FALSE;
}
/**
* Use this function to write to the $_SESSION global variable.
* This function prevents session blocking.
* If the value is NULL, the session variable will be unset.
*
* @param string|array $var A string representing the key or nested keys (e.g., 'key1->key2->key3') or an array of keys.
* @param mixed $value The value to set or NULL to unset the session variable.
*/
function session_set_var($var, $value)
{
// Extract nested keys if they exist
$keys = is_array($var) ? $var : explode('->', $var);
// Start the session (unblock)
@session_start();
// Check if the session variable is unchanged and return early if it is
if (get_value_by_keys($_SESSION, $keys) === $value) {
return;
}
// Set or unset the session variable based on the value
if (is_null($value)) {
unset_value_by_keys($_SESSION, $keys);
} else {
set_value_by_keys($_SESSION, $keys, $value);
}
// Commit the session (write and block)
session_write_close();
}
/**
* Unset a session variable using the provided key or nested keys.
*
* @param string $var A string representing the key or nested keys (e.g., 'key1->key2->key3').
*/
function session_unset_var($var)
{
session_set_var($var, NULL);
}
// Cookies
function cookie_get_auth_password($user_unique_id, $debug = FALSE) {
if (OBS_ENCRYPT && isset($_COOKIE['ckey']) && is_string($_COOKIE['ckey'])) {
///DEBUG
if ($debug) {
$debug_log = $GLOBALS['config']['log_dir'] . '/debug_cookie_' . date("Y-m-d_H:i:s") . '.log';
file_put_contents($debug_log, var_export($_SERVER, TRUE), FILE_APPEND);
file_put_contents($debug_log, var_export($_SESSION, TRUE), FILE_APPEND);
file_put_contents($debug_log, var_export($_COOKIE, TRUE), FILE_APPEND);
}
$ckey = dbFetchRow("SELECT * FROM `users_ckeys` WHERE `user_uniq` = ? AND `user_ckey` = ? LIMIT 1",
[ $user_unique_id, $_COOKIE['ckey'] ]);
if (is_array($ckey) && $ckey['expire'] > get_time() && session_allow_cidr()) {
// If userlevel == 0 - user disabled and can not be logon
if (auth_user_level($ckey['username']) < 1) {
session_logout(FALSE, 'User disabled');
reauth_with_message('User login disabled');
return FALSE;
}
session_set_var('username', $ckey['username']);
$auth_password = decrypt($ckey['user_encpass'], $_COOKIE['dkey']);
// Store encrypted password
session_encrypt_password($auth_password, $user_unique_id);
session_set_var('user_ckey_id', $ckey['user_ckey_id']);
session_set_var('cookie_auth', TRUE);
dbInsert([ 'user' => $_SESSION['username'],
'address' => session_remote_address(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'result' => 'Logged In (cookie)' ], 'authlog');
//logfile('wui_auth_cookie.log', var_export($_SERVER, TRUE)); ///DEBUG
return $auth_password;
}
}
return FALSE;
}
function cookie_set_keys($user_unique_id, $auth_password, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, $cookie_httponly = FALSE) {
if (OBS_ENCRYPT && isset($_POST['remember'])) {
$ckey = md5(random_string());
$dkey = md5(random_string());
$encpass = encrypt($auth_password, $dkey);
dbDelete('users_ckeys', "`username` = ? AND `expire` < ?", [ $_SESSION['username'], get_time() - 3600 ]); // Remove old ckeys from DB
dbInsert(['user_encpass' => $encpass,
'expire' => $cookie_expire,
'username' => $_SESSION['username'],
'user_uniq' => $user_unique_id,
'user_ckey' => $ckey], 'users_ckeys');
// AJAX request not have access to cookies with httponly, and for example widgets lost auth
//setcookie("ckey", $ckey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, $cookie_httponly);
//setcookie("dkey", $dkey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, $cookie_httponly);
setcookie("ckey", $ckey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, FALSE);
setcookie("dkey", $dkey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, FALSE);
session_unset_var('user_ckey_id');
}
}
function cookie_auth($cookie_expire) {
if (isset($_SESSION['cookie_auth']) && $_SESSION['cookie_auth']) {
@session_start();
$_SESSION['authenticated'] = TRUE;
dbUpdate([ 'expire' => $cookie_expire ], 'users_ckeys', '`user_ckey_id` = ?', [ $_SESSION['user_ckey_id'] ]);
unset($_SESSION['user_ckey_id'], $_SESSION['cookie_auth']);
session_write_close();
return TRUE;
}
return FALSE;
}
function cookie_clean_user() {
if (isset($_COOKIE['password'])) {
setcookie("password", NULL);
}
if (isset($_COOKIE['username'])) {
setcookie("username", NULL);
}
if (isset($_COOKIE['user_id'])) {
setcookie("user_id", NULL);
}
}
/**
* Every time you call session_start(), PHP adds another
* identical session cookie to the response header. Do this
* enough times, and your response header becomes big enough
* to choke the web server.
*
* This method clears out the duplicate session cookies. You can
* call it after each time you've called session_start(), or call it
* just before you send your headers.
*/
function clear_duplicate_cookies()
{
// If headers have already been sent, there's nothing we can do
if (headers_sent()) {
return;
}
$cookies = [];
foreach (headers_list() as $header) {
// Identify cookie headers
if (str_starts_with($header, 'Set-Cookie:')) {
$cookies[] = $header;
}
}
// Removes all cookie headers, including duplicates
header_remove('Set-Cookie');
// Restore one copy of each cookie
foreach (array_unique($cookies) as $cookie) {
header($cookie, FALSE);
}
}
/**
* Redirects to the front page with the specified authentication failure message.
* In the case of 'remote', no redirect is performed (as this would create an infinite loop,
* as there is no way to logout), so the message is simply printed.
*
* @param string $message Message to display to the user
*/
function reauth_with_message($message)
{
global $config;
// Detect AJAX request, do not write any messages or redirects there!
if (OBS_API || OBS_AJAX) {
// FIXME. But probably here required redirect to requested ajax page with params..
return;
}
if ($config['auth_mechanism'] === 'remote') {
print('<h1>' . $message . '</h1>');
} elseif (!empty($_GET['lm']) && is_numeric($_GET['lc']) && $_GET['lc'] > 0) {
// Already redirected page, prevent redirect loop
return;
} else {
//$redirect_count = !empty($_GET['lm']) && is_numeric($_GET['lc']) ? $_GET['lc']++ : 1;
session_set_var('auth_message', $message);
//redirect_to_url($config['base_url'] . '?lm='.var_encode($message));
// Message encrypted for prevent hijacking any custom messages
redirect_to_url($config['base_url'] . '?lm=' . encrypt($message, OBSERVIUM_PRODUCT . OBSERVIUM_VERSION) . '&lc=1');
}
exit();
}
function web_debug_log_message() {
return " URL=[" . $_SERVER['REQUEST_URI'] . "]. UA=[" . $_SERVER['HTTP_USER_AGENT'] . "]. REFERER=[" . $_SERVER['HTTP_REFERER'] . "].";
}
// EOF