Files
xxx/session.lib.php
T
tim ee20dc6eda Store persistent-login keys as SHA-256 hashes.
Each device now has its own row. Editors must log in again once after
deploying.
2026-09-25 21:42:34 +02:00

188 lines
6.3 KiB
PHP

<?php
require_once(realpath(dirname(__FILE__) . '/open_db.lib.php'));
require_once(__DIR__ . '/html.lib.php');
$is_admin = FALSE;
if (isset($_SERVER['HTTPS'])) {
session_cache_limiter('private');
session_set_cookie_params(array('lifetime' => strtotime('+1 year'), 'path' => '/', 'domain' => '.xxx.org.za', 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
session_start();
if (isset($_SESSION['editor'])) {
$is_admin = TRUE;
$username = $_SESSION['editor'];
} elseif (check_persistent_login()) {
// Note that check_persistent_login() sets $_SESSION['editor']
$is_admin = TRUE;
$username = $_SESSION['editor'];
} else {
$is_admin = FALSE;
}
// Don't cache if $is_admin is set
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Cache-Control: post-check=0, pre-check=0', false );
header( 'Pragma: no-cache' );
}
/*
* Persistent logins: the cookie holds "username|key", and the database
* holds only a SHA-256 of the key, so a copy of the table cannot be used
* to log in. Each device has its own row.
*/
function persistent_cookie() {
$cookie_data = $_COOKIE['PersistentAuth'] ?? '';
if (!is_string($cookie_data))
$cookie_data = '';
return explode("|", $cookie_data, 2) + array('', '');
}
function persistent_key_hash($key) {
return hash('sha256', $key);
}
function check_persistent_login() {
if (!array_key_exists('PersistentAuth', $_COOKIE)) {
return false;
}
global $db, $is_admin;
$table = "persistent_sessions";
list($username, $key) = persistent_cookie();
if ($username === '' || $key === '')
return false;
$key_hash = persistent_key_hash($key);
open_db('open');
$query = "SELECT * FROM $table WHERE username = ? AND session = ? LIMIT 1";
$getid = $db->execute_query($query, array($username, $key_hash)) or die("Query failed: ".$db->error);
$row = $getid->fetch_assoc();
open_db('close');
if (isset($row) && hash_equals((string)$row['session'], $key_hash)) {
// Log in, then swap the used key for a new one
login_editor($username);
delete_persistent_login();
create_persistent_login($username);
return true;
}
return false;
}
function create_persistent_login($username) {
$validity = strtotime('+1 year'); // Time to keep the session valid
global $db, $is_admin;
$table = "persistent_sessions";
open_db('open');
$key = bin2hex(random_bytes(32));
// Add the hash of the session key to the database
$query = "INSERT INTO $table (username, session) VALUES (?, ?)";
$db->execute_query($query, array($username, persistent_key_hash($key))) or die("Query failed: ".$db->error);
open_db('close');
// Create cookie with key
setcookie('PersistentAuth', "$username|$key", array('expires' => $validity, 'path' => '/', 'domain' => '.xxx.org.za', 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
return true;
}
function delete_persistent_login() {
global $db, $is_admin;
$table = "persistent_sessions";
open_db('open');
list($username, $key) = persistent_cookie();
// Remove this session key from the database
if ($username !== '' && $key !== '') {
$query = "DELETE FROM $table WHERE username = ? AND session = ?";
$db->execute_query($query, array($username, persistent_key_hash($key))) or die("Query failed: ".$db->error);
}
// Reap old sessions, and keys stored before they were hashed
$query = "DELETE FROM $table " .
" WHERE created < DATE_SUB(now(), INTERVAL 1 YEAR)" .
" OR CHAR_LENGTH(session) <> 64;";
$db->query($query) or die("Query failed: ".$db->error);
open_db('close');
// Delete cookie
setcookie("PersistentAuth", '', array('expires' => strtotime('-1 year'), 'path' => '/', 'domain' => '.xxx.org.za', 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
return true;
}
/*
* CSRF protection: every form that changes something carries a random
* token from the session, and the handler checks it before acting.
*/
function csrf_token() {
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token']))
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
return $_SESSION['csrf_token'];
}
function csrf_field() {
return '<input type="hidden" name="csrf_token" value="'.csrf_token().'" />';
}
function csrf_valid($token = NULL) {
if ($token === NULL)
$token = $_POST['csrf_token'] ?? '';
return is_string($token) && isset($_SESSION['csrf_token']) &&
hash_equals((string)$_SESSION['csrf_token'], $token);
}
/*
* Log an editor in under a new session id, so that a session id that
* someone planted or saw before the login is of no use afterwards.
*/
function login_editor($username) {
session_regenerate_id(TRUE);
unset($_SESSION['csrf_token']);
$_SESSION['editor'] = $username;
}
/*
* Password hashing: Argon2id where PHP was built with it, otherwise
* bcrypt. Argon2id hashes are about 100 characters long, so the
* editors.password column must hold at least that (VARCHAR(255) is best).
*/
function password_algorithm() {
return defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT;
}
/*
* Store $hash as the password of $username, and return TRUE if the database kept
* the whole hash. The database must be open.
*/
function store_password_hash($username, $hash) {
global $db;
$db->execute_query("UPDATE editors SET password = ? WHERE username = ?", array($hash, $username));
return password_hash_is_stored($username, $hash);
}
// TRUE if the database holds exactly $hash for $username
function password_hash_is_stored($username, $hash) {
global $db;
$check = $db->execute_query("SELECT password FROM editors WHERE username = ?", array($username));
$row = $check ? $check->fetch_assoc() : NULL;
return $row && $row['password'] === $hash;
}
/*
* After a successful login, move an old hash (MD5-crypt, bcrypt) to the
* current algorithm. If the column is too short and MySQL cut the new
* hash, put the old one back so that the editor can still log in.
*/
function rehash_password($username, $password, $stored_hash) {
if (!password_needs_rehash($stored_hash, password_algorithm()))
return;
if (!store_password_hash($username, password_hash($password, password_algorithm()))) {
store_password_hash($username, $stored_hash);
error_log("xxx: could not store a new password hash for $username; widen editors.password");
}
}