From e8f071f67ce658cf1f25e65a4cf07d82a020cb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Sat, 4 Oct 2014 20:46:19 +0200 Subject: [PATCH] Rework session handling; add CSRF tokens and autologin --- includes/config.inc.php-dist | 5 +- includes/session.inc.php | 330 +++++++++++++++++++++++++++-------- includes/wefactauth.inc.php | 2 - index.php | 43 +++-- users.php | 4 +- zones.php | 4 +- 6 files changed, 295 insertions(+), 93 deletions(-) diff --git a/includes/config.inc.php-dist b/includes/config.inc.php-dist index 9efc436..bd0c2cb 100644 --- a/includes/config.inc.php-dist +++ b/includes/config.inc.php-dist @@ -20,6 +20,9 @@ $allowzoneadd = FALSE; # Allow normal users to add zones $authdb = "../etc/pdns.users.sqlite3"; +# Set a random generated secret to enable auto-login and long living csrf tokens +// $secret = '...'; + $templates = array(); /* $templates[] = array( @@ -57,5 +60,3 @@ if (!file_exists($authdb)) { $salt = bin2hex(openssl_random_pseudo_bytes(16)); $db->exec("INSERT INTO users (emailaddress, password, isadmin) VALUES ('admin', '".crypt("admin", '$6$'.$salt)."', 1)"); } - -?> diff --git a/includes/session.inc.php b/includes/session.inc.php index 51e5719..0af9f89 100644 --- a/includes/session.inc.php +++ b/includes/session.inc.php @@ -4,102 +4,286 @@ include_once('config.inc.php'); include_once('misc.inc.php'); include_once('wefactauth.inc.php'); -session_start(); +global $current_user; -function is_logged_in() { - if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] == "true") { - return TRUE; +$current_user = false; + +// session startup +function _set_current_user($username, $is_admin = false, $has_csrf_token = false, $is_api = false) { + global $current_user; + + $current_user = array( + 'username' => $username, + 'is_admin' => $is_admin, + 'has_csrf_token' => $has_csrf_token, + 'is_api' => $is_api, + ); +} + +function _check_csrf_token($user) { + global $secret; + + if (isset($_SERVER['HTTP_X_CSRF_TOKEN']) && $_SERVER['HTTP_X_CSRF_TOKEN']) { + $found_token = $_SERVER['HTTP_X_CSRF_TOKEN']; + } elseif (isset($_POST['csrf-token']) && $_POST['csrf-token']) { + $found_token = $_POST['csrf-token']; } else { - global $adminapikey; - global $adminapiips; + $found_token = ''; + } - if (isset($adminapikey) && isset($adminapiips)) { - if (array_search($_SERVER['REMOTE_ADDR'], $adminapiips) !== FALSE) { - if ($_POST['adminapikey'] == $adminapikey) { - # Allow this request, fake that we're logged in. - set_logged_in('admin'); - set_is_adminuser(); - $_SESSION['apientrance'] = 'true'; - return TRUE; - } - } + if (isset($secret) && $secret) { + # if we have a secret keep csrf-token valid across logins + $csrf_hmac_secret = hash_pbkdf2('sha256', 'csrf_hmac', $secret, 100, 0, true); + $userinfo = base64_encode($user['emailaddress']) . ':' . base64_encode($user['password']); + $csrf_token = base64_encode(hash_hmac('sha256', $userinfo, $csrf_hmac_secret, true)); + } else { + # without secret create new token for each session + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = base64_encode(openssl_random_pseudo_bytes(32)); } - return FALSE; + $csrf_token = $_SESSION['csrf_token']; } -} -function set_apiuser() { - $_SESSION['apientrance'] = 'true'; -} - -function is_apiuser() { - if (isset($_SESSION['apientrance']) && $_SESSION['apientrance'] = 'true') { - return TRUE; + if ($found_token === $csrf_token) { + global $current_user; + $current_user['has_csrf_token'] = true; } - return FALSE; + + define('CSRF_TOKEN', $csrf_token); + header("X-CSRF-Token: ${csrf_token}"); } -function set_logged_in($login_user) { - $_SESSION['logged_in'] = 'true'; - $_SESSION['username'] = $login_user; -} +function enc_secret($message) { + global $secret; -function set_is_adminuser() { - $_SESSION['is_adminuser'] = 'true'; -} + if (isset($secret) && $secret) { + $enc_secret = hash_pbkdf2('sha256', 'encryption', $secret, 100, 0, true); + $hmac_secret = hash_pbkdf2('sha256', 'encryption_hmac', $secret, 100, 0, true); -function is_adminuser() { - if (isset($_SESSION['is_adminuser']) && $_SESSION['is_adminuser'] == 'true') { - return TRUE; - } else { - return FALSE; + $mcrypt = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, '') or die('missing mcrypt'); + + # add PKCS#7 padding + $blocksize = mcrypt_get_block_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $padlength = $blocksize - (strlen($message) % $blocksize); + $message .= str_repeat(chr($padlength), $padlength); + + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND); + $ciphertext = $iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $enc_secret, $message, MCRYPT_MODE_CBC, $iv); + mcrypt_module_close($mcrypt); + + $mac = hash_hmac('sha256', $ciphertext, $hmac_secret, true); + return 'enc:' . base64_encode($ciphertext) . ':' . base64_encode($mac); } + + return base64_encode($message); } -function get_sess_user() { - return $_SESSION['username']; +function dec_secret($code) { + global $secret; + $is_encrypted = (substr($code, 0, 4) === 'enc:'); + if (isset($secret) && $secret) { + if (!$is_encrypted) return false; + + $msg = explode(':', $code); + if (3 != count($msg)) return false; + + $enc_secret = hash_pbkdf2('sha256', 'encryption', $secret, 100, 0, true); + $hmac_secret = hash_pbkdf2('sha256', 'encryption_hmac', $secret, 100, 0, true); + + $msg[1] = base64_decode($msg[1]); + $msg[2] = base64_decode($msg[2]); + + $mac = hash_hmac('sha256', $msg[1], $hmac_secret, true); + # compare hashes first: this should prevent any timing leak + if (hash('sha256', $mac, true) !== hash('sha256', $msg[2], true)) return false; + if ($mac !== $msg[2]) return false; + + $mcrypt = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, '') or die('missing mcrypt'); + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv = substr($msg[1], 0, $iv_size); + $ciphertext = substr($msg[1], $iv_size); + $plaintext = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $enc_secret, $ciphertext, MCRYPT_MODE_CBC, $iv); + mcrypt_module_close($mcrypt); + + # remove PKCS#7 padding + $len = strlen($plaintext); + $padlength = ord($plaintext[$len-1]); + $plaintext = substr($plaintext, 0, $len - $padlength); + + return $plaintext; + } + + if ($is_encrypted) return false; + return base64_decode($code); } -function logout() { - session_destroy(); +function _unset_cookie($name) { + $is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + setcookie($name, null, -1, null, null, $is_ssl); +} + +function _store_auto_login($value) { + $is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + // set for 30 days + setcookie('NSEDIT_AUTOLOGIN', $value, time()+60*60*24*30, null, null, $is_ssl); } function try_login() { - global $wefactapiurl; - global $wefactapikey; - if (isset($_POST['username']) and isset($_POST['password'])) { - if (valid_user($_POST['username']) === FALSE) { - return FALSE; - } - $do_local_auth = 1; + if (_try_login($_POST['username'], $_POST['password'])) { + global $secret; - if (isset($wefactapiurl) && isset($wefactapikey)) { - $wefact = do_wefact_auth($_POST['username'], $_POST['password']); - if ($wefact === FALSE) { - return FALSE; - } - if ($wefact !== -1) { - $do_local_auth = 0; + # only store if we have a secret. + if ($secret && isset($_POST['autologin']) && $_POST['autologin']) { + _store_auto_login(enc_secret(json_encode(array( + 'username' => $_POST['username'], + 'password' => $_POST['password'])))); } + return true; } - - if ($do_local_auth == 1) { - if (do_db_auth($_POST['username'], $_POST['password']) === FALSE) { - return FALSE; - } - } - - $userinfo = get_user_info($_POST['username']); - - set_logged_in($_POST['username']); - if (isset($userinfo['isadmin']) && $userinfo['isadmin'] == 1) { - set_is_adminuser(); - } - return TRUE; } - - return FALSE; + return false; } -?> +function _try_login($username, $password) { + global $wefactapiurl, $wefactapikey; + + if (!valid_user($username)) { + return false; + } + + $do_local_auth = true; + + if (isset($wefactapiurl) && isset($wefactapikey)) { + $wefact = do_wefact_auth($username, $password); + if (false === $wefact ) { + return false; + } + if (-1 !== $wefact) { + $do_local_auth = false; + } + } + + if ($do_local_auth && !do_db_auth($username, $password)) { + return false; + } + + $user = get_user_info($username); + if (!$user) { + return false; + } else { + _set_current_user($username, (bool) $user['isadmin']); + + if (session_id()) { + session_unset(); + session_destroy(); + } + session_start() or die('session failure: could not start session'); + session_regenerate_id(true) or die('session failure: regenerated id failed'); + session_unset(); + $_SESSION['username'] = $username; + + # requires session: + _check_csrf_token($user); + return true; + } +} + +function _check_session() { + global $adminapikey, $adminapiips; + + $is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + session_set_cookie_params(30*60, null, null, $is_ssl, true); + session_name('NSEDIT_SESSION'); + + if (isset($adminapikey) && '' !== $adminapikey && isset($adminapiips) && isset($_POST['adminapikey'])) { + if (false !== array_search($_SERVER['REMOTE_ADDR'], $adminapiips) + and $_POST['adminapikey'] === $adminapikey) + { + # Allow this request, fake that we're logged in as user. + return _set_current_user('admin', true, true, true); + } + else + { + header('Status: 403 Forbidden'); + exit(0); + } + } + + if (isset($_COOKIE['NSEDIT_SESSION'])) { + if (session_start() && isset($_SESSION['username'])) { + $user = get_user_info($_SESSION['username']); + if (!$user) { + session_destroy(); + session_unset(); + } else { + _set_current_user($_SESSION['username'], (bool) $user['isadmin']); + _check_csrf_token($user); + return; + } + } + // failed: remove cookie + _unset_cookie('NSEDIT_SESSION'); + } + + if (isset($_COOKIE['NSEDIT_AUTOLOGIN'])) { + $login = json_decode(dec_secret($_COOKIE['NSEDIT_AUTOLOGIN']), 1); + if ($login and isset($login['username']) and isset($login['password']) + and _try_login($login['username'], $login['password'])) { + _store_auto_login($_COOKIE['NSEDIT_AUTOLOGIN']); # reset cookie + return; + } + + // failed: remove cookie + _unset_cookie('NSEDIT_AUTOLOGIN'); + } +} + +# auto load session if possible +_check_session(); + +function is_logged_in() { + global $current_user; + return (bool) $current_user; +} + +# GET/HEAD requests only require a logged in user (they shouldn't trigger any +# "writes"); all other requests require the X-CSRF-Token to be present. +function is_csrf_safe() { + global $current_user; + + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + case 'HEAD': + return is_logged_in(); + default: + return (bool) $current_user && (bool) $current_user['has_csrf_token']; + } +} + +function is_apiuser() { + global $current_user; + return $current_user && (bool) $current_user['is_api']; +} + +function is_adminuser() { + global $current_user; + return $current_user && (bool) $current_user['is_admin']; +} + +function get_sess_user() { + global $current_user; + return $current_user ? $current_user['username'] : null; +} + +function logout() { + @session_destroy(); + @session_unset(); + if (isset($_COOKIE['NSEDIT_AUTOLOGIN'])) { + _unset_cookie('NSEDIT_AUTOLOGIN'); + } + if (isset($_COOKIE['NSEDIT_SESSION'])) { + _unset_cookie('NSEDIT_SESSION'); + } +} diff --git a/includes/wefactauth.inc.php b/includes/wefactauth.inc.php index 517dcab..43adc07 100644 --- a/includes/wefactauth.inc.php +++ b/includes/wefactauth.inc.php @@ -78,5 +78,3 @@ function do_wefact_auth($u, $p) { return -1; } } - -?> diff --git a/index.php b/index.php index 438a5a0..db142e1 100644 --- a/index.php +++ b/index.php @@ -7,12 +7,11 @@ include_once('includes/misc.inc.php'); if (isset($_GET['logout']) or isset($_POST['logout'])) { logout(); header("Location: index.php"); + exit(0); } -if (!is_logged_in() and isset($_POST['formname']) && $_POST['formname'] == "loginform") { - if (try_login() === TRUE) { - set_logged_in($_POST['username']); - } else { +if (!is_logged_in() and isset($_POST['formname']) and $_POST['formname'] === "loginform") { + if (!try_login()) { $errormsg = "Error while trying to authenticate you\n"; } } @@ -54,18 +53,28 @@ if (!is_logged_in()) { - + - + + + + + + + - +
Username:
Password:
Remember me:
- + @@ -113,6 +122,21 @@ exit(0); - - + \ No newline at end of file diff --git a/users.php b/users.php index 7dc7548..0e0a2e1 100644 --- a/users.php +++ b/users.php @@ -4,7 +4,7 @@ include_once('includes/config.inc.php'); include_once('includes/session.inc.php'); include_once('includes/misc.inc.php'); -if (!is_logged_in()) { +if (!is_csrf_safe()) { header('Status: 403'); header('Location: ./index.php'); jtable_respond(null, 'error', "Authentication required"); @@ -96,5 +96,3 @@ default: jtable_respond(null, 'error', 'Invalid action'); break; } - -?> diff --git a/zones.php b/zones.php index b2d48f6..952cd58 100644 --- a/zones.php +++ b/zones.php @@ -4,7 +4,7 @@ include_once('includes/config.inc.php'); include_once('includes/session.inc.php'); include_once('includes/misc.inc.php'); -if (!is_logged_in()) { +if (!is_csrf_safe()) { header('Status: 403'); header('Location: ./index.php'); jtable_respond(null, 'error', "Authentication required"); @@ -605,5 +605,3 @@ default: jtable_respond(null, 'error', 'No such action'); break; } - -?>