0) { $secret = hash('sha512', $token, false); db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ? AND token = ?', [$user_id, $secret]); } // Revoke the server-side credential before clearing browser state. cacti_cookie_session_logout(); } } /** * set_auth_cookie - sets a users security token * * @param array $user Is the user_auth row for the user * * @return void */ function set_auth_cookie(array $user) : void { if (db_table_exists('user_auth_cache')) { clear_auth_cookie(); $nssecret = bin2hex(random_bytes(32)); $secret = hash('sha512', $nssecret, false); db_execute_prepared('REPLACE INTO user_auth_cache (user_id, hostname, last_update, token) VALUES (?, ?, NOW(), ?);', [$user['id'], get_client_addr(), $secret]); cacti_cookie_session_set($user['id'], $user['realm'], $nssecret); } } /** * Clears the authentication cookie and removes the user session from the cache. * * @return int|false The user of the session cookie, otherwise false */ function check_auth_cookie() : int|false { if (isset($_COOKIE['cacti_remembers']) && read_config_option('auth_cache_enabled') == 'on' && db_table_exists('user_auth_cache')) { if (!is_string($_COOKIE['cacti_remembers'])) { return false; } $parts = explode(',', $_COOKIE['cacti_remembers']); if (cacti_sizeof($parts) == 2) { $user_id = $parts[0]; $realm_id = -1; $token = $parts[1]; } elseif (cacti_sizeof($parts) == 3) { $user_id = $parts[0]; $realm_id = $parts[1]; $token = $parts[2]; } else { return false; } if (!is_numeric($user_id)) { $user_id = db_fetch_cell_prepared('SELECT id FROM user_auth WHERE username = ?', [$user_id]); } if ($user_id > 0 && $user_id !== get_guest_account()) { if ($realm_id == -1) { $user_info = db_fetch_row_prepared('SELECT * FROM user_auth WHERE id = ?', [$user_id]); } else { $user_info = db_fetch_row_prepared('SELECT * FROM user_auth WHERE id = ? AND realm = ?', [$user_id, $realm_id]); } if (cacti_sizeof($user_info)) { $secret = hash('sha512', $token, false); // GHSA-xq26-f282-f998: reject remember-me tokens past the 90-day retention window instead of trusting the cron purge alone. $found = db_fetch_cell_prepared('SELECT user_id FROM user_auth_cache WHERE user_id = ? AND token = ? AND last_update >= DATE_SUB(NOW(), INTERVAL 90 DAY)', [$user_info['id'], $secret] ); if (empty($found)) { return false; } // a disabled account must not authenticate from a cookie issued before it was disabled if ($user_info['enabled'] != 'on') { db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_info['id']]); return false; } if (!auth_cookie_user_currently_allowed($user_info)) { db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_info['id']]); return false; } set_auth_cookie($user_info); cacti_log(sprintf('LOGIN: User %s Authenticated via Authentication Cookie from IP Address %s', $user_info['username'], get_client_addr()), false, 'AUTH'); db_execute_prepared('INSERT IGNORE INTO user_log (username, user_id, result, ip, time) VALUES (?, ?, 2, ?, NOW())', [$user_info['username'], $user_info['id'], get_client_addr()] ); return $user_info['id']; } } } return false; } /** * Confirms that a remember-me cookie still belongs to an enabled account * with current access before the cookie is allowed to create a new session. * * @param array $user_info The user_auth row loaded from the cookie identity. * * @return bool True when the account can still authenticate with the cookie. */ function auth_cookie_user_currently_allowed(array $user_info) : bool { if (($user_info['enabled'] ?? '') != 'on') { return false; } if (($user_info['locked'] ?? '') == 'on') { return false; } if (!auth_realm_allows_cookies((int) ($user_info['realm'] ?? 0))) { return false; } return auth_user_has_access($user_info); } /** * cacti_auth_transition - move a session from unauthenticated to authenticated. * * Issues a new session id so a session identifier planted by an attacker before * login cannot be reused afterwards, and drops the permission caches so the new identity is * evaluated from scratch rather than inheriting the previous one. * * Call this at every point that first sets the session user id, apart from the * guest account, which is not a privilege gain. * * @param int $user_id The account the session is becoming. * @param string $reason Where the transition came from, for the log. * * @return bool False when the account is locked and must not be let in. */ function cacti_auth_transition(int $user_id, string $reason = 'login') : bool { $locked = db_fetch_cell_prepared('SELECT locked FROM user_auth WHERE id = ?', [$user_id]); if ($locked === false || $locked == 'on') { cacti_log(sprintf('SECURITY: auth transition blocked for unavailable or locked user %d, reason %s', $user_id, $reason), false, 'AUTH'); return false; } if (session_status() === PHP_SESSION_ACTIVE) { if (!session_regenerate_id(true)) { cacti_log(sprintf('SECURITY: auth transition blocked because session regeneration failed for user %d, reason %s', $user_id, $reason), false, 'AUTH'); return false; } } kill_session_var(SESS_USER_REALMS); kill_session_var(SESS_AUTH_NAMES); kill_session_var(SESS_TREE_PERMS); kill_session_var(SESS_SIMPLE_PERMS); kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS); kill_session_var(SESS_USER_PERMS_KEY); kill_session_var(SESS_USER_2FA); kill_session_var(OPTIONS_USER); kill_session_var(OPTIONS_WEB); cacti_log(sprintf('NOTE: auth transition completed for user %d, reason %s', $user_id, $reason), false, 'AUTH', POLLER_VERBOSITY_MEDIUM); return true; } /** * Given a username or user_id test if this is a template account * Template accounts could be accounts used for the administrative email, for both * the guest and template accounts, or a user that is specified by a plugin as a * template account. * * @param null|int|string $user_id The user ID or username to check. * * @return bool True if the user is a template account, false otherwise. */ function is_template_account(null|int|string $user_id) : bool { if (is_string($user_id)) { $user_id = db_fetch_cell_prepared('SELECT id FROM user_auth WHERE username = ?', [$user_id]); } if (empty($user_id)) { return false; } if (read_config_option('admin_user') == $user_id) { return true; } if (read_config_option('guest_user') == $user_id) { return true; } if (read_config_option('user_template') == $user_id) { return true; } else { $domain_template = db_fetch_cell_prepared('SELECT COUNT(*) FROM login_providers WHERE user_id = ?', [$user_id]); if ($domain_template > 0) { return true; } else { $plugin_template = get_template_account($user_id); if ($plugin_template == $user_id) { return true; } } } return false; } /** * Whether the current request originates from a reverse proxy that config.php * lists in $trusted_proxies. Only such proxies may assert a pre-authenticated * user via forwarded HTTP headers. Matching is IP-normalized, so equivalent * spellings (for example ::1 and 0:0:0:0:0:0:0:1) match regardless of text * representation, while an IPv4-mapped entry never silently matches its bare * IPv4 form. CIDR entries such as 10.0.0.0/8 are accepted. A malformed or * non-IP entry falls back to exact string equality. An empty or unset list * trusts no proxy. * * @return bool true when REMOTE_ADDR matches a trusted proxy entry. */ function is_trusted_proxy() : bool { global $config; if (!isset($_SERVER['REMOTE_ADDR'])) { return false; } $trusted = $config['trusted_proxies'] ?? []; if (!is_array($trusted) || !cacti_sizeof($trusted)) { return false; } return cacti_trusted_proxy_match((string) $_SERVER['REMOTE_ADDR'], $trusted); } /** * If basic auth is used, return the valid username * * @return string|false The username if found and processed, or false if no username is found. */ function get_basic_auth_username() : string|false { /* Headers arriving with an HTTP_ prefix are client-supplied and spoofable. * Only honor a forwarded identity when the request comes from a trusted * reverse proxy. The unprefixed REMOTE_USER / REDIRECT_REMOTE_USER vars are * set by the web server's own auth module and are always honored. */ $trusted = is_trusted_proxy(); if (isset($_SERVER['PHP_AUTH_USER'])) { $raw = is_array($_SERVER['PHP_AUTH_USER']) ? ($_SERVER['PHP_AUTH_USER'][0] ?? '') : $_SERVER['PHP_AUTH_USER']; $username = str_replace('\\', '\\\\', $raw); } elseif (isset($_SERVER['REMOTE_USER'])) { $raw = is_array($_SERVER['REMOTE_USER']) ? ($_SERVER['REMOTE_USER'][0] ?? '') : $_SERVER['REMOTE_USER']; $username = str_replace('\\', '\\\\', $raw); } elseif (isset($_SERVER['REDIRECT_REMOTE_USER'])) { $raw = is_array($_SERVER['REDIRECT_REMOTE_USER']) ? ($_SERVER['REDIRECT_REMOTE_USER'][0] ?? '') : $_SERVER['REDIRECT_REMOTE_USER']; $username = str_replace('\\', '\\\\', $raw); } elseif ($trusted && isset($_SERVER['HTTP_X_FORWARDED_USER'])) { $raw = is_array($_SERVER['HTTP_X_FORWARDED_USER']) ? ($_SERVER['HTTP_X_FORWARDED_USER'][0] ?? '') : $_SERVER['HTTP_X_FORWARDED_USER']; $username = str_replace('\\', '\\\\', $raw); } elseif ($trusted && isset($_SERVER['HTTP_PHP_AUTH_USER'])) { $raw = is_array($_SERVER['HTTP_PHP_AUTH_USER']) ? ($_SERVER['HTTP_PHP_AUTH_USER'][0] ?? '') : $_SERVER['HTTP_PHP_AUTH_USER']; $username = str_replace('\\', '\\\\', $raw); } elseif ($trusted && isset($_SERVER['HTTP_REMOTE_USER'])) { $raw = is_array($_SERVER['HTTP_REMOTE_USER']) ? ($_SERVER['HTTP_REMOTE_USER'][0] ?? '') : $_SERVER['HTTP_REMOTE_USER']; $username = str_replace('\\', '\\\\', $raw); } elseif ($trusted && isset($_SERVER['HTTP_REDIRECT_REMOTE_USER'])) { $raw = is_array($_SERVER['HTTP_REDIRECT_REMOTE_USER']) ? ($_SERVER['HTTP_REDIRECT_REMOTE_USER'][0] ?? '') : $_SERVER['HTTP_REDIRECT_REMOTE_USER']; $username = str_replace('\\', '\\\\', $raw); } else { $username = false; } if ($username !== false && is_string($username)) { if (str_contains($username, '@')) { $upart = explode('@', $username); $username = $upart[0]; } /* Handle mapping basic accounts to shortform accounts. * Format of map file is CSV: basic,shortform */ $mapfile = read_config_option('path_basic_mapfile'); if ($mapfile != '' && file_exists($mapfile) && is_readable($mapfile)) { $records = file($mapfile); $found = false; if (cacti_sizeof($records)) { foreach (($records ?: []) as $r) { if (trim($r) === '') { continue; } [$basic, $shortform] = str_getcsv($r); if ($basic === null || trim($basic) === '') { continue; } if (trim($basic) == $username) { $username = trim($shortform ?? ''); $found = true; break; } } } if (!$found) { cacti_log("WARNING: Username $username not found in basic mapfile.", false, 'AUTH'); } } } return is_string($username) ? $username : false; } /** * Copies a user from a template user to a new user, optionally overwriting an existing user. * * @param string $template_user The username of the template user to copy from. * @param string $new_user The username of the new user to create. * @param int $template_realm The realm of the template user. Default is 0. * @param int $new_realm The realm of the new user. Default is 0. * @param bool $overwrite Whether to overwrite an existing user with the same username and realm. Default is false. * @param array $data_override An associative array of fields to override in the new user's data. Default is an empty array. * * @return mixed - The ID of the new user if successful, or false if the operation failed. */ function user_copy(string $template_user, string $new_user, int $template_realm = 0, int $new_realm = 0, bool $overwrite = false, array $data_override = []) : mixed { // ================= input validation ================= input_validate_input_number($template_realm, 'template_realm'); input_validate_input_number($new_realm, 'new_realm'); // ==================================================== // Check get template users array $user_auth = db_fetch_row_prepared('SELECT * FROM user_auth WHERE username = ? AND realm = ?', [$template_user, $template_realm]); if (!cacti_sizeof($user_auth)) { return false; } $template_id = $user_auth['id']; // Create update/insert for new/existing user $user_exist = db_fetch_row_prepared('SELECT * FROM user_auth WHERE username = ? AND realm = ?', [$new_user, $new_realm]); if (cacti_sizeof($user_exist)) { if ($overwrite === true) { // Overwrite existing user $user_auth['id'] = $user_exist['id']; $user_auth['username'] = $user_exist['username']; $user_auth['password'] = $user_exist['password']; $user_auth['realm'] = $user_exist['realm']; $user_auth['full_name'] = $user_exist['full_name']; $user_auth['email_address'] = $user_exist['email_address']; $user_auth['must_change_password'] = $user_exist['must_change_password']; $user_auth['enabled'] = $user_exist['enabled']; } else { // User already exists, duplicate users are bad raise_message(19); return false; } } else { // new user $user_auth['id'] = 0; $user_auth['username'] = $new_user; $user_auth['enabled'] = 'on'; $user_auth['password'] = mt_rand(100000, 10000000); $user_auth['email_address'] = ''; $user_auth['realm'] = $new_realm; } // Update data_override fields if (cacti_sizeof($data_override)) { foreach ($data_override as $field => $value) { if (isset($user_auth[$field]) && $field != 'id' && $field != 'username') { $user_auth[$field] = $value; } } } // Save the user $new_id = sql_save($user_auth, 'user_auth'); // Create/Update permissions and settings if (cacti_sizeof($user_exist) && $overwrite === true) { db_execute_prepared('DELETE FROM user_auth_perms WHERE user_id = ?', [$user_exist['id']]); db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ?', [$user_exist['id']]); db_execute_prepared('DELETE FROM settings_user WHERE user_id = ?', [$user_exist['id']]); db_execute_prepared('DELETE FROM settings_tree WHERE user_id = ?', [$user_exist['id']]); } $user_auth_perms = db_fetch_assoc_prepared('SELECT * FROM user_auth_perms WHERE user_id = ?', [$template_id]); if (cacti_sizeof($user_auth_perms)) { foreach ($user_auth_perms as $row) { $row['user_id'] = $new_id; sql_save($row, 'user_auth_perms', ['user_id', 'item_id', 'type'], false); } } $user_auth_realm = db_fetch_assoc_prepared('SELECT * FROM user_auth_realm WHERE user_id = ?', [$template_id]); if (cacti_sizeof($user_auth_realm)) { foreach ($user_auth_realm as $row) { $row['user_id'] = $new_id; sql_save($row, 'user_auth_realm', ['realm_id', 'user_id'], false); } } $settings_user = db_fetch_assoc_prepared('SELECT * FROM settings_user WHERE user_id = ?', [$template_id]); if (cacti_sizeof($settings_user)) { foreach ($settings_user as $row) { $row['user_id'] = $new_id; sql_save($row, 'settings_user', ['user_id', 'name'], false); } } $settings_tree = db_fetch_assoc_prepared('SELECT * FROM settings_tree WHERE user_id = ?', [$template_id]); if (cacti_sizeof($settings_tree)) { foreach ($settings_tree as $row) { $row['user_id'] = $new_id; sql_save($row, 'settings_tree', ['user_id', 'graph_tree_item_id'], false); } } // apply group permissions for the user $groups = db_fetch_assoc_prepared('SELECT group_id FROM user_auth_group_members WHERE user_id = ?', [$template_id]); if (cacti_sizeof($groups)) { $sql = []; foreach ($groups as $g) { $sql[] = '(' . $new_id . ', ' . $g['group_id'] . ')'; } db_execute('INSERT IGNORE INTO user_auth_group_members (user_id, group_id) VALUES ' . implode(',', $sql)); } api_plugin_hook_function('copy_user', ['template_id' => $template_id, 'new_id' => $new_id]); return $new_id; } /** * Removes a user and all associated data from the database. * * @param int $user_id The ID of the user to be removed. * * @return void */ function user_remove(int $user_id) : void { // ================= input validation ================= input_validate_input_number($user_id, 'user_id'); // ==================================================== // check for guest or template user $username = db_fetch_cell_prepared('SELECT username FROM user_auth WHERE id = ?', [$user_id]); if ($username != gnrv('username')) { if (is_template_account($user_id)) { raise_message(21); return; } if ($user_id === get_guest_account()) { raise_message(21); return; } } db_execute_prepared('DELETE FROM user_auth WHERE id = ?', [$user_id]); db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM user_auth_perms WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM user_auth_row_cache WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM user_auth_group_members WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM settings_user WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM settings_tree WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM user_layouts WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM sessions WHERE user_id = ?', [$user_id]); api_plugin_hook_function('user_remove', $user_id); } /** * Disable a user account * * @param int $user_id The ID of the user to disable. * * @return void */ function user_disable(int $user_id) : void { // ================= input validation ================= input_validate_input_number($user_id, 'user_id'); // ==================================================== db_execute_prepared("UPDATE user_auth SET enabled = '' WHERE id = ?", [$user_id]); db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM user_auth_row_cache WHERE user_id = ?', [$user_id]); db_execute_prepared('DELETE FROM sessions WHERE user_id = ?', [$user_id]); reset_user_perms($user_id); } /** * Enable a user account * * @param int $user_id The ID of the user to enable. * * @return void */ function user_enable(int $user_id) : void { // ================= input validation ================= input_validate_input_number($user_id, 'user_id'); // ==================================================== db_execute_prepared("UPDATE user_auth SET enabled = 'on' WHERE id = ?", [$user_id]); reset_user_perms($user_id); } /** * Replace every outstanding password-reset token for a local user. * * Locking the user row serializes the self-service and administrator issuance * paths, so concurrent requests cannot leave multiple valid reset links. * * @param int $user_id The local user receiving the token * @param string $hash Cryptographically random reset-token hash * @param int $timeout_minutes Token lifetime in minutes * * @return bool True when the replacement token was committed */ function auth_reset_token_replace(int $user_id, string $hash, int $timeout_minutes) : bool { if ($user_id <= 0 || $hash === '' || $timeout_minutes <= 0 || !db_begin_transaction()) { return false; } $locked_user = db_fetch_cell_prepared('SELECT id FROM user_auth WHERE id = ? AND realm = 0 AND enabled = "on" FOR UPDATE', [$user_id]); $success = (int) $locked_user === $user_id && db_execute_prepared('DELETE FROM user_auth_reset_hashes WHERE user_id = ?', [$user_id]) !== false && db_execute_prepared('INSERT INTO user_auth_reset_hashes (user_id, hash, expiry) VALUES (?, ?, date_add(now(), interval ? minute))', [$user_id, $hash, $timeout_minutes]) !== false; if ($success && db_commit_transaction()) { return true; } db_rollback_transaction(); return false; } /** * Retrieves the authentication realms based on the configured authentication method. * * @param bool $login Whether the function is being called during a login process. Default is false. * * @return array An array of authentication realms */ function get_auth_realms(bool $login = false) : array { if (read_config_option('auth_method') == AUTH_METHOD_PROVIDERS) { // SAML2/OpenID are redirect-only and cannot be driven from this username // + password realm dropdown; they get their own login page buttons instead. $drealms = db_fetch_assoc('SELECT id, name FROM login_providers WHERE enabled="on" AND type IN (' . PROVIDER_TYPE_LDAP . ', ' . PROVIDER_TYPE_AD . ') ORDER BY name'); $drealms = is_array($drealms) ? $drealms : []; if (cacti_sizeof($drealms)) { if ($login) { $new_realms['0'] = [ 'name' => __('Local'), 'selected' => false ]; foreach ($drealms as $realm) { $new_realms[1000 + $realm['id']] = [ 'name' => $realm['name'], 'selected' => false ]; } $default_realm = db_fetch_cell('SELECT id FROM login_providers WHERE is_default=1 AND enabled="on" AND type IN (' . PROVIDER_TYPE_LDAP . ', ' . PROVIDER_TYPE_AD . ')'); if (!empty($default_realm)) { $new_realms[1000 + $default_realm]['selected'] = true; } else { $new_realms['0']['selected'] = true; } } else { $new_realms['0'] = __('Local'); foreach ($drealms as $realm) { $new_realms[1000 + $realm['id']] = $realm['name']; } } return $new_realms; } else { $new_realms['0'] = [ 'name' => __('Local'), 'selected' => false ]; return $new_realms; } } // TODO: Verify this array return [ '0' => ['name' => __('Local')], '3' => ['name' => __('LDAP')], '2' => ['name' => __('Web Basic')], ]; } /** * Returns the enabled SAML2/OpenID Login Providers, for rendering as * "Login with