mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Security: end a user's other sessions when their password changes - each session carries a stamp of the password it signed in with and the agent and client portal checks sign out any that no longer match, covering a self change, an admin setting it, the incident-response reset, portal reset links and invites, and View as contact; a portal self change keeps the browser that made it on a new session id
This commit is contained in:
@@ -42,7 +42,12 @@ if ($portal_view_as) {
|
||||
}
|
||||
|
||||
// Load user session vars
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_archived_at, user_auth_method, user_avatar, user_status, user_token, user_type FROM users WHERE users.user_id = $session_user_id"));
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_archived_at, user_auth_method, user_avatar, user_password, user_status, user_token, user_type FROM users WHERE users.user_id = $session_user_id"));
|
||||
|
||||
$session_stamp_current = userSessionStamp($row['user_password'] ?? '');
|
||||
if ($row) {
|
||||
unset($row['user_password']);
|
||||
}
|
||||
|
||||
$session_avatar = $row['user_avatar'] ?? '';
|
||||
$session_auth_method = (string) ($row['user_auth_method'] ?? '');
|
||||
@@ -82,6 +87,21 @@ if (!$portal_view_as) {
|
||||
session_destroy();
|
||||
redirect("/login.php");
|
||||
}
|
||||
|
||||
// Check the password hasn't changed since this session signed in - by the contact
|
||||
// elsewhere, an agent, or a reset link (userSessionStamp())
|
||||
if (!hash_equals($session_stamp_current, (string) ($_SESSION['user_session_stamp'] ?? ''))) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
redirect("/login.php");
|
||||
}
|
||||
|
||||
} elseif (!hash_equals((string) $portal_view_as['agent_session_stamp'], (string) ($_SESSION['user_session_stamp'] ?? ''))) {
|
||||
|
||||
// View as contact runs on the agent's session, so their password changing ends it too
|
||||
session_unset();
|
||||
session_destroy();
|
||||
redirect("/login.php");
|
||||
}
|
||||
|
||||
// Two-step sign-in required for password accounts (Admin > Client Portal) and
|
||||
|
||||
@@ -126,7 +126,7 @@ if (isset($_GET['code']) || isset($_GET['error'])) {
|
||||
|
||||
$upn = mysqli_real_escape_string($mysqli, $msgraph_response["userPrincipalName"]);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_client_id, contact_id, user_auth_method, user_email, user_id FROM users
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_client_id, contact_id, user_auth_method, user_email, user_id, user_password FROM users
|
||||
LEFT JOIN contacts ON user_id = contact_user_id
|
||||
LEFT JOIN clients ON contact_client_id = client_id
|
||||
WHERE user_email = '$upn'
|
||||
@@ -156,6 +156,8 @@ if (isset($_GET['code']) || isset($_GET['error'])) {
|
||||
$_SESSION['contact_id'] = $contact_id;
|
||||
$_SESSION['csrf_token'] = randomString(32);
|
||||
$_SESSION['login_method'] = "azure";
|
||||
// A later password change ends this session (userSessionStamp())
|
||||
$_SESSION['user_session_stamp'] = userSessionStamp($row['user_password'] ?? '');
|
||||
unset($_SESSION['portal_view_as']); // an agent's View as contact in this browser ends here
|
||||
|
||||
// Logging
|
||||
|
||||
@@ -562,9 +562,11 @@ if (isset($_POST['portal_password_change'])) {
|
||||
redirect('profile.php#security');
|
||||
}
|
||||
|
||||
$hash = escapeSql(password_hash($new_password, PASSWORD_DEFAULT));
|
||||
mysqli_query($mysqli, "UPDATE users SET user_password = '$hash' WHERE user_id = $session_user_id AND user_type = 2");
|
||||
$new_hash = password_hash($new_password, PASSWORD_DEFAULT);
|
||||
mysqli_query($mysqli, "UPDATE users SET user_password = '" . escapeSql($new_hash) . "' WHERE user_id = $session_user_id AND user_type = 2");
|
||||
portalPasswordLinkClear($session_user_id);
|
||||
// This browser stays signed in, with a new session id - every other session of theirs ends
|
||||
userSessionRestamp($session_user_id, $new_hash);
|
||||
|
||||
logAudit("Contact", "Edit", "Client contact $session_contact_name changed their password in the client portal", $session_client_id, $session_contact_id);
|
||||
portalMailAccountChange(stripslashes($session_contact_email), stripslashes($session_contact_name), 'password');
|
||||
|
||||
@@ -343,7 +343,7 @@ function portalViewAsAccess($agent_user_id, $contact_id) {
|
||||
if (intval($config_client_portal_enable) !== 1 || !$agent_user_id || !$contact_id) {
|
||||
return null;
|
||||
}
|
||||
$agent = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name, user_type, user_status, user_archived_at, user_role_id, role_is_admin
|
||||
$agent = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name, user_password, user_type, user_status, user_archived_at, user_role_id, role_is_admin
|
||||
FROM users LEFT JOIN user_roles ON role_id = user_role_id WHERE user_id = $agent_user_id LIMIT 1"));
|
||||
if (!$agent || intval($agent['user_type']) !== 1 || intval($agent['user_status']) !== 1 || $agent['user_archived_at'] !== null) {
|
||||
return null;
|
||||
@@ -391,6 +391,7 @@ function portalViewAsAccess($agent_user_id, $contact_id) {
|
||||
'user_id' => intval($contact['contact_user_id']),
|
||||
'agent_user_id' => $agent_user_id,
|
||||
'agent_name' => (string) $agent['user_name'],
|
||||
'agent_session_stamp' => userSessionStamp($agent['user_password'] ?? ''), // client/includes/check_login.php
|
||||
'billing' => $level('module_sales') >= 1 && $level('module_financial') >= 1,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -202,9 +202,34 @@ function userPasswordProblem($password) {
|
||||
*/
|
||||
function userSetPassword($user_id, $password) {
|
||||
global $mysqli;
|
||||
$hash = escapeSql(password_hash((string) $password, PASSWORD_DEFAULT));
|
||||
$hash = password_hash((string) $password, PASSWORD_DEFAULT);
|
||||
$ciphertext = escapeSql(encryptUserSpecificKey((string) $password));
|
||||
mysqli_query($mysqli, "UPDATE users SET user_password = '$hash', user_specific_encryption_ciphertext = '$ciphertext' WHERE user_id = " . intval($user_id));
|
||||
mysqli_query($mysqli, "UPDATE users SET user_password = '" . escapeSql($hash) . "', user_specific_encryption_ciphertext = '$ciphertext' WHERE user_id = " . intval($user_id));
|
||||
userSessionRestamp($user_id, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* What a session keeps of the password it was signed in with - a digest of the hash,
|
||||
* never the hash. A new password makes every session signed in before it stop
|
||||
* matching, and the next page each one loads signs it out: includes/load_user_session.php
|
||||
* for agents, client/includes/check_login.php for the portal. That covers every way a
|
||||
* password is set - by its owner, by an admin, the incident-response reset, a portal
|
||||
* reset link - with nothing to call, the same way disabling a user already ends their
|
||||
* sessions.
|
||||
*/
|
||||
function userSessionStamp($password_hash) {
|
||||
return hash('sha256', 'itflow-session|' . (string) $password_hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep this browser signed in when the password just changed is its own user's - a new
|
||||
* session id, and the stamp of the new hash. Every other session of theirs still ends.
|
||||
*/
|
||||
function userSessionRestamp($user_id, $password_hash) {
|
||||
if (session_status() === PHP_SESSION_ACTIVE && intval($_SESSION['user_id'] ?? 0) === intval($user_id) && !headers_sent()) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_session_stamp'] = userSessionStamp($password_hash);
|
||||
}
|
||||
}
|
||||
|
||||
/** Turn an agent's MFA off - and forget the browsers it trusted, which have to sign in afresh. */
|
||||
|
||||
@@ -8,7 +8,7 @@ $sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT role_is_admin, role_name, user_archived_at, user_avatar, user_config_force_mfa,
|
||||
user_config_records_per_page, user_config_theme_dark, user_email, user_name, user_role_id,
|
||||
user_status, user_token, user_type FROM users
|
||||
user_password, user_status, user_token, user_type FROM users
|
||||
LEFT JOIN user_settings ON users.user_id = user_settings.user_id
|
||||
LEFT JOIN user_roles ON user_role_id = role_id
|
||||
WHERE users.user_id = $session_user_id"
|
||||
@@ -16,6 +16,11 @@ $sql = mysqli_query(
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
|
||||
$session_stamp_current = userSessionStamp($row['user_password'] ?? '');
|
||||
if ($row) {
|
||||
unset($row['user_password']);
|
||||
}
|
||||
|
||||
$session_name = escapeSql($row['user_name']);
|
||||
$session_email = $row['user_email'];
|
||||
$session_avatar = $row['user_avatar'];
|
||||
@@ -51,6 +56,14 @@ if ($session_user_archived_at !== null) {
|
||||
redirect("/login.php");
|
||||
}
|
||||
|
||||
// Check the password hasn't changed since this session signed in - by them elsewhere,
|
||||
// an admin, or the incident-response reset (userSessionStamp())
|
||||
if (!hash_equals($session_stamp_current, (string) ($_SESSION['user_session_stamp'] ?? ''))) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
redirect("/login.php");
|
||||
}
|
||||
|
||||
// Load user client permissions (allow + deny lists)
|
||||
$user_client_access_sql = "SELECT client_id, permission_type FROM user_client_permissions WHERE user_id = $session_user_id";
|
||||
$user_client_access_result = mysqli_query($mysqli, $user_client_access_sql);
|
||||
|
||||
@@ -471,6 +471,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
||||
$_SESSION['user_id'] = $user_id;
|
||||
$_SESSION['csrf_token'] = randomString(32);
|
||||
$_SESSION['logged'] = true;
|
||||
// A later password change ends this session (userSessionStamp())
|
||||
$_SESSION['user_session_stamp'] = userSessionStamp($selectedRow['user_password'] ?? '');
|
||||
|
||||
if ($force_mfa == 1 && $token == NULL) {
|
||||
$config_start_page = "user/mfa_enforcement.php";
|
||||
@@ -710,6 +712,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['login']) || isset($_
|
||||
$_SESSION['user_type'] = 2;
|
||||
$_SESSION['contact_id'] = $contact_id;
|
||||
$_SESSION['login_method'] = "local";
|
||||
// A later password change ends this session (userSessionStamp())
|
||||
$_SESSION['user_session_stamp'] = userSessionStamp($selectedRow['user_password'] ?? '');
|
||||
|
||||
// Keep consistent with agent flow (helps any shared session checks)
|
||||
$_SESSION['logged'] = true;
|
||||
|
||||
Reference in New Issue
Block a user