From 39ce0e7dfc8082ec2a544cdc35ba30508a3735eb Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 30 Apr 2024 11:34:53 +0200 Subject: [PATCH 1/7] auth.php: trim --- inc/mod/auth.php | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 493149c8..43297173 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -9,14 +9,14 @@ defined('TINYBOARD') or exit; // create a hash/salt pair for validate logins function mkhash($username, $password, $salt = false) { global $config; - + if (!$salt) { // create some sort of salt for the hash $salt = substr(base64_encode(sha1(rand() . time(), true) . $config['cookies']['salt']), 0, 15); - + $generated_salt = true; } - + // generate hash (method is not important as long as it's strong) $hash = substr( base64_encode( @@ -30,7 +30,7 @@ function mkhash($username, $password, $salt = false) { ) ), 0, 20 ); - + if (isset($generated_salt)) return array($hash, $salt); else @@ -81,11 +81,11 @@ function generate_salt() { function login($username, $password) { global $mod, $config; - + $query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username"); $query->bindValue(':username', $username); $query->execute() or error(db_error($query)); - + if ($user = $query->fetch(PDO::FETCH_ASSOC)) { list($version, $ok) = test_password($user['password'], $user['version'], $password); @@ -109,7 +109,7 @@ function login($username, $password) { ); } } - + return false; } @@ -117,10 +117,10 @@ function setCookies() { global $mod, $config; if (!$mod) error('setCookies() was called for a non-moderator!'); - + setcookie($config['cookies']['mod'], $mod['username'] . // username - ':' . + ':' . $mod['hash'][0] . // password ':' . $mod['hash'][1], // salt @@ -147,36 +147,36 @@ function modLog($action, $_board=null) { else $query->bindValue(':board', null, PDO::PARAM_NULL); $query->execute() or error(db_error($query)); - + if ($config['syslog']) _syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action); } function create_pm_header() { global $mod, $config; - + if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) { if ($header === true) return false; - + return $header; } - + $query = prepare("SELECT `id` FROM ``pms`` WHERE `to` = :id AND `unread` = 1"); $query->bindValue(':id', $mod['id'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + if ($pm = $query->fetch(PDO::FETCH_ASSOC)) $header = array('id' => $pm['id'], 'waiting' => $query->rowCount() - 1); else $header = true; - + if ($config['cache']['enabled']) cache::set('pm_unread_' . $mod['id'], $header); - + if ($header === true) return false; - + return $header; } @@ -197,12 +197,12 @@ function check_login($prompt = false) { if ($prompt) mod_login(); exit; } - + $query = prepare("SELECT `id`, `type`, `boards`, `password` FROM ``mods`` WHERE `username` = :username"); $query->bindValue(':username', $cookie[0]); $query->execute() or error(db_error($query)); $user = $query->fetch(PDO::FETCH_ASSOC); - + // validate password hash if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) { // Malformed cookies @@ -210,7 +210,7 @@ function check_login($prompt = false) { if ($prompt) mod_login(); exit; } - + $mod = array( 'id' => (int)$user['id'], 'type' => (int)$user['type'], From 5a378dd605ff82e42c1af57abd94954329187dfe Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 29 Apr 2024 16:46:51 +0200 Subject: [PATCH 2/7] config.php: reduce default login cookie expire timeout --- inc/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/config.php b/inc/config.php index c34eb034..03e6044e 100644 --- a/inc/config.php +++ b/inc/config.php @@ -186,7 +186,7 @@ // How long should the cookies last (in seconds). Defines how long should moderators should remain logged // in (0 = browser session). - $config['cookies']['expire'] = 60 * 60 * 24 * 30 * 6; // ~6 months + $config['cookies']['expire'] = 60 * 60 * 24 * 7; // 1 week. // Make this something long and random for security. $config['cookies']['salt'] = 'abcdefghijklmnopqrstuvwxyz09123456789!@#$%^&*()'; From abdf82e1c89563fc3de55a201b3507da41f297df Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 30 Apr 2024 11:41:19 +0200 Subject: [PATCH 3/7] auth.php: remove obsolete code --- inc/mod/auth.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 43297173..3b62559e 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -37,12 +37,6 @@ function mkhash($username, $password, $salt = false) { return $hash; } -function crypt_password_old($password) { - $salt = generate_salt(); - $password = hash('sha256', $salt . sha1($password)); - return array($salt, $password); -} - function crypt_password($password) { global $config; // `salt` database field is reused as a version value. We don't want it to be 0. @@ -69,13 +63,6 @@ function test_password($password, $salt, $test) { } function generate_salt() { - // mcrypt_create_iv() was deprecated in PHP 7.1.0, only use it if we're below that version number. - if (PHP_VERSION_ID < 70100) { - // 128 bits of entropy - return strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.'); - } - - // Otherwise, use random_bytes() return strtr(base64_encode(random_bytes(16)), '+', '.'); } From 9db8444c3cfea34a6eb8d8ffa64d2777373a749f Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 29 Apr 2024 16:46:03 +0200 Subject: [PATCH 4/7] auth.php: use secured names and directives for mod cookies --- inc/mod/auth.php | 80 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 3b62559e..d9073b50 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -66,6 +66,18 @@ function generate_salt() { return strtr(base64_encode(random_bytes(16)), '+', '.'); } +function calc_cookie_name($is_https, $is_path_jailed, $base_name) { + if ($is_https) { + if ($is_path_jailed) { + return "__Host-$base_name"; + } else { + return "__Secure-$base_name"; + } + } else { + return $base_name; + } +} + function login($username, $password) { global $mod, $config; @@ -102,22 +114,62 @@ function login($username, $password) { function setCookies() { global $mod, $config; - if (!$mod) + if (!$mod) { error('setCookies() was called for a non-moderator!'); + } - setcookie($config['cookies']['mod'], - $mod['username'] . // username - ':' . - $mod['hash'][0] . // password - ':' . - $mod['hash'][1], // salt - time() + $config['cookies']['expire'], $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', $config['cookies']['httponly']); + $is_https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + $is_path_jailed = $config['cookies']['jail']; + $name = calc_cookie_name($is_https, $is_path_jailed, $config['cookies']['mod']); + + // :: + $value = "{$mod['username']}:{$mod['hash'][0]}:{$mod['hash'][1]}"; + + $options = [ + 'expires' => time() + $config['cookies']['expire'], + 'path' => $is_path_jailed ? $config['cookies']['path'] : '/', + 'secure' => $is_https, + 'httponly' => $config['cookies']['httponly'], + 'samesite' => 'Strict' + ]; + + setcookie($name, $value, $options); } function destroyCookies() { global $config; - // Delete the cookies - setcookie($config['cookies']['mod'], 'deleted', time() - $config['cookies']['expire'], $config['cookies']['jail']?$config['cookies']['path'] : '/', null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); + $base_name = $config['cookies']['mod']; + $del_time = time() - 60 * 60 * 24 * 365; // 1 year. + $jailed_path = $config['cookies']['jail'] ? $config['cookies']['path'] : '/'; + $http_only = $config['cookies']['httponly']; + + $options_multi = [ + $base_name => [ + 'expires' => $del_time, + 'path' => $jailed_path , + 'secure' => false, + 'httponly' => $http_only, + 'samesite' => 'Strict' + ], + "__Host-$base_name" => [ + 'expires' => $del_time, + 'path' => $jailed_path, + 'secure' => true, + 'httponly' => $http_only, + 'samesite' => 'Strict' + ], + "__Secure-$base_name" => [ + 'expires' => $del_time, + 'path' => '/', + 'secure' => true, + 'httponly' => $http_only, + 'samesite' => 'Strict' + ] + ]; + + foreach ($options_multi as $name => $options) { + setcookie($name, 'deleted', $options); + } } function modLog($action, $_board=null) { @@ -174,10 +226,14 @@ function make_secure_link_token($uri) { function check_login($prompt = false) { global $config, $mod; + $is_https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + $is_path_jailed = $config['cookies']['jail']; + $expected_cookie_name = calc_cookie_name($is_https, $is_path_jailed, $config['cookies']['mod']); + // Validate session - if (isset($_COOKIE[$config['cookies']['mod']])) { + if (isset($expected_cookie_name)) { // Should be username:hash:salt - $cookie = explode(':', $_COOKIE[$config['cookies']['mod']]); + $cookie = explode(':', $_COOKIE[$expected_cookie_name]); if (count($cookie) != 3) { // Malformed cookies destroyCookies(); From 0c51d46cdf5bc84ccdb9b95543985511d98234b7 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 30 Apr 2024 15:58:42 +0200 Subject: [PATCH 5/7] auth.php: check if the cookie is set before deletion --- inc/mod/auth.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index d9073b50..4cc20df5 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -168,7 +168,10 @@ function destroyCookies() { ]; foreach ($options_multi as $name => $options) { - setcookie($name, 'deleted', $options); + if (isset($_COOKIE[$name])) { + setcookie($name, 'deleted', $options); + unset($_COOKIE[$name]); + } } } From da4842eb7b9e7c78d14fdd2731d59855a4ec2a81 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 30 Apr 2024 11:31:06 +0200 Subject: [PATCH 6/7] auth.php: disallow unencrypted logins by default --- composer.json | 1 + inc/config.php | 5 +++++ inc/functions/net.php | 10 ++++++++++ inc/mod/auth.php | 7 +++++-- inc/mod/pages.php | 9 +++++++-- 5 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 inc/functions/net.php diff --git a/composer.json b/composer.json index b06ff6a1..e5b73d9b 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "inc/lock.php", "inc/queue.php", "inc/functions.php", + "inc/functions/net.php", "inc/driver/http-driver.php", "inc/driver/log-driver.php", "inc/service/captcha-queries.php" diff --git a/inc/config.php b/inc/config.php index 03e6044e..2b271ee2 100644 --- a/inc/config.php +++ b/inc/config.php @@ -194,6 +194,10 @@ // Whether or not you can access the mod cookie in JavaScript. Most users should not need to change this. $config['cookies']['httponly'] = true; + // Do not allow logins via unencrypted HTTP. Should only be changed in testing environments or if you connect to a + // load-balancer without encryption. + $config['cookies']['secure_login_only'] = true; + // Used to salt secure tripcodes ("##trip") and poster IDs (if enabled). $config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba'; @@ -1252,6 +1256,7 @@ // Moderator errors $config['error']['toomanyunban'] = _('You are only allowed to unban %s users at a time. You tried to unban %u users.'); $config['error']['invalid'] = _('Invalid username and/or password.'); + $config['error']['insecure'] = _('Login on insecure connections is disabled.'); $config['error']['notamod'] = _('You are not a mod…'); $config['error']['invalidafter'] = _('Invalid username and/or password. Your user may have been deleted or changed.'); $config['error']['malformed'] = _('Invalid/malformed cookies.'); diff --git a/inc/functions/net.php b/inc/functions/net.php new file mode 100644 index 00000000..ab08c3cb --- /dev/null +++ b/inc/functions/net.php @@ -0,0 +1,10 @@ + Date: Tue, 30 Apr 2024 19:34:26 +0200 Subject: [PATCH 7/7] auth.php: add typing --- inc/mod/auth.php | 65 +++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index e299fdb5..c96a0487 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -9,7 +9,7 @@ use Vichan\Functions\Net; defined('TINYBOARD') or exit; // create a hash/salt pair for validate logins -function mkhash($username, $password, $salt = false) { +function mkhash(string $username, string $password, bool $salt = false): array|string { global $config; if (!$salt) { @@ -33,42 +33,40 @@ function mkhash($username, $password, $salt = false) { ), 0, 20 ); - if (isset($generated_salt)) - return array($hash, $salt); - else + if (isset($generated_salt)) { + return [ $hash, $salt ]; + } else { return $hash; + } } -function crypt_password($password) { +function crypt_password(string $password): array { global $config; // `salt` database field is reused as a version value. We don't want it to be 0. $version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1; $new_salt = generate_salt(); $password = crypt($password, $config['password_crypt'] . $new_salt . "$"); - return array($version, $password); + return [ $version, $password ]; } -function test_password($password, $salt, $test) { - global $config; - +function test_password(string $password, string $salt, string $test): array { // Version = 0 denotes an old password hashing schema. In the same column, the // password hash was kept previously - $version = (strlen($salt) <= 8) ? (int) $salt : 0; + $version = strlen($salt) <= 8 ? (int)$salt : 0; if ($version == 0) { $comp = hash('sha256', $salt . sha1($test)); - } - else { + } else { $comp = crypt($test, $password); } - return array($version, hash_equals($password, $comp)); + return [ $version, hash_equals($password, $comp) ]; } -function generate_salt() { +function generate_salt(): string { return strtr(base64_encode(random_bytes(16)), '+', '.'); } -function calc_cookie_name($is_https, $is_path_jailed, $base_name) { +function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_name): string { if ($is_https) { if ($is_path_jailed) { return "__Host-$base_name"; @@ -80,7 +78,7 @@ function calc_cookie_name($is_https, $is_path_jailed, $base_name) { } } -function login($username, $password) { +function login(string $username, string $password): array|false { global $mod, $config; $query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username"); @@ -101,20 +99,20 @@ function login($username, $password) { $query->execute() or error(db_error($query)); } - return $mod = array( + return $mod = [ 'id' => $user['id'], 'type' => $user['type'], 'username' => $username, 'hash' => mkhash($username, $user['password']), 'boards' => explode(',', $user['boards']) - ); + ]; } } return false; } -function setCookies() { +function setCookies(): void { global $mod, $config; if (!$mod) { error('setCookies() was called for a non-moderator!'); @@ -138,7 +136,7 @@ function setCookies() { setcookie($name, $value, $options); } -function destroyCookies() { +function destroyCookies(): void { global $config; $base_name = $config['cookies']['mod']; $del_time = time() - 60 * 60 * 24 * 365; // 1 year. @@ -177,7 +175,7 @@ function destroyCookies() { } } -function modLog($action, $_board=null) { +function modLog(string $action, ?string $_board = null): void { global $mod, $board, $config; $query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)"); $query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT); @@ -192,16 +190,18 @@ function modLog($action, $_board=null) { $query->bindValue(':board', null, PDO::PARAM_NULL); $query->execute() or error(db_error($query)); - if ($config['syslog']) + if ($config['syslog']) { _syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action); + } } -function create_pm_header() { +function create_pm_header(): mixed { global $mod, $config; if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) { - if ($header === true) + if ($header === true) { return false; + } return $header; } @@ -210,26 +210,29 @@ function create_pm_header() { $query->bindValue(':id', $mod['id'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); - if ($pm = $query->fetch(PDO::FETCH_ASSOC)) - $header = array('id' => $pm['id'], 'waiting' => $query->rowCount() - 1); - else + if ($pm = $query->fetch(PDO::FETCH_ASSOC)) { + $header = [ 'id' => $pm['id'], 'waiting' => $query->rowCount() - 1 ]; + } else { $header = true; + } - if ($config['cache']['enabled']) + if ($config['cache']['enabled']) { cache::set('pm_unread_' . $mod['id'], $header); + } - if ($header === true) + if ($header === true) { return false; + } return $header; } -function make_secure_link_token($uri) { +function make_secure_link_token(string $uri): string { global $mod, $config; return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8); } -function check_login($prompt = false) { +function check_login(bool $prompt = false): void { global $config, $mod; $is_https = Net\is_connection_secure();