diff --git a/composer.json b/composer.json index 83504521..6968a43f 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "gregwar/captcha": "v1.2.1", "mxrxdxn/pwned-passwords": "v2.1.0", "components/jquery": "v3.7.1", - "fortawesome/font-awesome": "6.5.1" + "fortawesome/font-awesome": "6.5.1", + "symfony/cache": "v5.4.42" }, "scripts": { "post-update-cmd": [ diff --git a/conf/config.inc.php b/conf/config.inc.php index 7e7f54ad..afd72eb5 100644 --- a/conf/config.inc.php +++ b/conf/config.inc.php @@ -247,6 +247,18 @@ # Token lifetime in seconds $token_lifetime = "3600"; +## Cache +# $cache_token_expiration: integer, duration in seconds of cached objects +# each time a token is involved +# (for example when sending a token by sms or by mail) +# it is recommended to set a value >= $token_lifetime +$cache_token_expiration = 3600; +# $cache_form_expiration: integer, duration in seconds of cached objects +# at some steps when a user has to validate a form +# (for example when validating the email address before we send the mail) +# it is recommended to set a value high enough for a user to fill a form +$cache_form_expiration = 120; + # Reset URL (mandatory) $reset_url = "http://ssp.example.com/"; # If inside a virtual host diff --git a/docs/config_general.rst b/docs/config_general.rst index 3e32a60d..dc3e2697 100644 --- a/docs/config_general.rst +++ b/docs/config_general.rst @@ -316,6 +316,34 @@ See `FriendlyCaptcha documentation `_ for mor You can also integrate any other Captcha module by developping the corresponding plugin. (see :doc:`developpers` ) +.. _config_cache: + +Cache +----- + +self-service-password rely on Symfony cache libraries. + +You can define the cache expiration for some objects: + +.. code-block:: php + + $cache_token_expiration = 3600; + +``$cache_token_expiration`` (integer) is the duration in seconds of cached objects each time a token is involved. + +For example when sending a token by sms or by mail, it is the time granted to the user for entering the sms code or for clicking on the link in the mail. + +it is recommended to set a value >= ``$token_lifetime`` + +.. code-block:: php + + $cache_form_expiration = 120; + +``$cache_form_expiration`` (integer) is the duration in seconds of cached objects at some steps when a user has to validate a form. + +For example it is the time granted to a user for validating the email address before sending the mail. It is used mainly for avoiding form replay (by user mistake or by a hacker). + +it is recommended to set a value high enough for a user to fill a form. .. |image0| image:: images/br.png .. |image1| image:: images/catalonia.png diff --git a/docs/config_sms.rst b/docs/config_sms.rst index 079d9502..1a722880 100644 --- a/docs/config_sms.rst +++ b/docs/config_sms.rst @@ -201,3 +201,12 @@ You can also configure the allowed attempts: $sms_max_attempts_token = 3; After these attempts, the sent token is no more valid. + +You should also set a token lifetime, so they are invalid after some time. The +value is in seconds: + +.. code-block:: php + + $token_lifetime = "3600"; + +If you use tokens, you should also set :ref:`config_cache` parameters accordingly. diff --git a/docs/config_tokens.rst b/docs/config_tokens.rst index 898473c3..c2cbfb54 100644 --- a/docs/config_tokens.rst +++ b/docs/config_tokens.rst @@ -50,15 +50,14 @@ You can crypt tokens, to protect the session identifier: .. warning:: If you enable this option, you must change the default value of the security keyphrase. -You should set a token lifetime, so they are deleted if unused. The +You should set a token lifetime, so they are invalid after some time. The value is in seconds: .. code-block:: php $token_lifetime = "3600"; -.. warning:: Token deletion is managed by PHP session garbage - collector. +If you use tokens, you should also set :ref:`config_cache` parameters accordingly. Log --- diff --git a/docs/upgrade.rst b/docs/upgrade.rst index b02e1840..154df7c3 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -1,6 +1,37 @@ Upgrade ======= +From 1.6 to 1.7 +--------------- + +If you have configured ``$token_lifetime`` parameter, for example for reset by sms or reset by mail features, you should verify that the duration is coherent with the new cache parameters, and adapt these parameters in your local configuration file if needed: + +.. code-block:: php + + # $cache_token_expiration: integer, duration in seconds of cached objects + # each time a token is involved + # (for example when sending a token by sms or by mail) + # it is recommended to set a value >= $token_lifetime + $cache_token_expiration = 3600; + # $cache_form_expiration: integer, duration in seconds of cached objects + # at some steps when a user has to validate a form + # (for example when validating the email address before we send the mail) + # it is recommended to set a value high enough for a user to fill a form + $cache_form_expiration = 120; + + +New bundled dependencies have been added: + +* php-symfony-deprecation-contracts = v2.5.3 +* php-symfony-var-exporter = v5.4.40 +* php-psr-container = 1.1.2 +* php-symfony-service-contracts = v2.5.3 +* php-psr-cache = 1.0.1 +* php-symfony-cache-contracts = v2.5.3 +* php-psr-log = 1.1.4 +* php-symfony-cache = v5.4.42 + + From 1.5 to 1.6 --------------- diff --git a/htdocs/index.php b/htdocs/index.php index 4ea09b34..7813b0ff 100644 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -16,6 +16,8 @@ require_once("../vendor/autoload.php"); require_once("../lib/functions.inc.php"); +use Symfony\Component\Cache\Adapter\FilesystemAdapter; + #============================================================================== # VARIABLES #============================================================================== @@ -119,6 +121,16 @@ isset($ldap_krb5ccname) ? $ldap_krb5ccname : null ); +#============================================================================== +# Cache Config +#============================================================================== +$sspCache = new FilesystemAdapter( + $namespace = 'sspCache', + $defaultLifetime = 0, + $directory = null +); +$sspCache->prune(); + #============================================================================== # Captcha Config #============================================================================== diff --git a/htdocs/resetbytoken.php b/htdocs/resetbytoken.php index 83777ac4..bf0f4f70 100644 --- a/htdocs/resetbytoken.php +++ b/htdocs/resetbytoken.php @@ -52,16 +52,15 @@ $tokenid = $token; } - # select internal session by $tokenid without relying on cookie or url + # select token in the cache # will gather login,time and smstoken values from session. - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",1); - - session_id($tokenid); - session_name("token"); - session_start(); - $login = $_SESSION['login']; - $smstoken = isset($_SESSION['smstoken']) ? $_SESSION['smstoken'] : false; + $cached_token = $sspCache->getItem($tokenid); + $cached_token_content = $cached_token->get(); + if($cached_token->isHit()) + { + $login = $cached_token_content['login']; + } + $smstoken = isset($cached_token_content['smstoken']) ? $cached_token_content['smstoken'] : false; $posttoken = isset($_REQUEST['smstoken']) ? $_REQUEST['smstoken'] : 'undefined'; if ( !$login ) { @@ -72,7 +71,7 @@ error_log("Token not associated with SMS code ".$posttoken); } else if (isset($token_lifetime)) { # Manage lifetime with session content - $tokentime = $_SESSION['time']; + $tokentime = $cached_token_content['time']; if ( time() - $tokentime > $token_lifetime ) { $result = "tokennotvalid"; error_log("Token lifetime expired"); @@ -179,8 +178,7 @@ # Delete token if all is ok if ( $result === "passwordchanged" ) { - $_SESSION = array(); - session_destroy(); + $sspCache->deleteItem($tokenid); } #============================================================================== diff --git a/htdocs/sendsms.php b/htdocs/sendsms.php index 2791a609..84581889 100644 --- a/htdocs/sendsms.php +++ b/htdocs/sendsms.php @@ -83,15 +83,16 @@ $tokenid = decrypt($token, $keyphrase); - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",1); - - session_id($tokenid); - session_name("smstoken"); - session_start(); - $login = $_SESSION['login']; - $sessiontoken = $_SESSION['smstoken']; - $attempts = $_SESSION['attempts']; + # Get session from cache + $cached_token = $sspCache->getItem($tokenid); + $cached_token_content = $cached_token->get(); + + if($cached_token->isHit()) + { + $login = $cached_token_content['login']; + $sessiontoken = $cached_token_content['smstoken']; + $attempts = $cached_token_content['attempts']; + } if (!$login or !$sessiontoken) { list($result, $token) = obscure_info_sendsms("tokenattempts", @@ -99,11 +100,13 @@ $token, $obscure_notfound_sendsms, $keyphrase); - error_log("Unable to open session $smstokenid"); + error_log("Unable to open session $tokenid"); } elseif ($sessiontoken != $smstoken) { # To have only x tries and not x+1 tries if ($attempts < ($sms_max_attempts_token - 1)) { - $_SESSION['attempts'] = $attempts + 1; + $cached_token_content['attempts'] = $attempts + 1; + $cached_token->set($cached_token_content); + $sspCache->save($cached_token); $result = "tokenattempts"; error_log("SMS token $smstoken not valid, attempt $attempts"); } else { @@ -111,19 +114,19 @@ error_log("SMS token $smstoken not valid"); } } elseif (isset($token_lifetime)) { - $tokentime = $_SESSION['time']; + $tokentime = $cached_token_content['time']; if ( time() - $tokentime > $token_lifetime ) { $result = "tokennotvalid"; error_log("Token lifetime expired"); } } if ( $result === "tokennotvalid" ) { - $_SESSION = array(); - session_destroy(); + # Remove token + $sspCache->deleteItem($tokenid); } if ( $result === "" ) { - $_SESSION = array(); - session_destroy(); + # Remove token + $sspCache->deleteItem($tokenid); $result = "buildtoken"; } } elseif (isset($_REQUEST["encrypted_sms_login"])) { @@ -203,15 +206,17 @@ # Generate sms token $smstoken = generate_sms_token($sms_token_length); # Create temporary session to avoid token replay - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",1); - - session_name("smstoken"); - session_start(); - $_SESSION['login'] = $login; - $_SESSION['smstoken'] = $smstoken; - $_SESSION['time'] = time(); - $_SESSION['attempts'] = 0; + $smstoken_session_id = hash('sha256', bin2hex(random_bytes(16))); + $smscached_token = $sspCache->getItem($smstoken_session_id); + $smscached_token->set([ + 'login' => $login, + 'smstoken' => $smstoken, + 'time' => time(), + 'attempts' => 0 + ]); + $smscached_token->expiresAfter($cache_token_expiration); + $sspCache->save($smscached_token); + error_log("generated cache entry with id: " . $smstoken_session_id. " for storing step 'send sms' of password reset by sms workflow, valid for $cache_token_expiration s"); $data = array( "sms_attribute" => $sms, "smsresetmessage" => $messages['smsresetmessage'], "smstoken" => $smstoken) ; @@ -220,7 +225,7 @@ if ($sms_method === "mail") { if ($mailer->send_mail($smsmailto, $mail_from, $mail_from_name, $smsmail_subject, $sms_message, $data)) { - $token = encrypt(session_id(), $keyphrase); + $token = encrypt($smstoken_session_id, $keyphrase); $result = "smssent"; if (!empty($reset_request_log)) { error_log("Send SMS code $smstoken by $sms_method to $sms\n\n", 3, $reset_request_log); @@ -244,7 +249,7 @@ $definedVariables = get_defined_vars(); // get all variables, including configuration $smsInstance = createSMSInstance($sms_api_lib, $definedVariables); if ($smsInstance->send_sms_by_api($sms, $sms_message)) { - $token = encrypt(session_id(), $keyphrase); + $token = encrypt($smstoken_session_id, $keyphrase); $result = "smssent"; if ( !empty($reset_request_log) ) { error_log("Send SMS code $smstoken by $sms_method to $sms\n\n", 3, $reset_request_log); @@ -264,18 +269,18 @@ #============================================================================== if ($result === "buildtoken") { - # Use PHP session to register token - # We do not generate cookie - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",1); - - session_name("token"); - session_start(); - $_SESSION['login'] = $login; - $_SESSION['time'] = time(); - $_SESSION['smstoken'] = $smstoken; - - $token = encrypt(session_id(), $keyphrase); + $smstoken_session_id = hash('sha256', bin2hex(random_bytes(16))); + $smscached_token = $sspCache->getItem($smstoken_session_id); + $smscached_token->set([ + 'login' => $login, + 'time' => time(), + 'smstoken' => $smstoken + ]); + $smscached_token->expiresAfter($cache_form_expiration); + $sspCache->save($smscached_token); + error_log("generated cache entry with id: " . $smstoken_session_id. " for storing step 'password change' of password reset by sms workflow, valid for $cache_form_expiration s"); + + $token = encrypt($smstoken_session_id, $keyphrase); $result = "redirect"; } diff --git a/htdocs/sendtoken.php b/htdocs/sendtoken.php index 826d4745..e1377102 100644 --- a/htdocs/sendtoken.php +++ b/htdocs/sendtoken.php @@ -58,16 +58,12 @@ $result = "emptysendtokenform"; # Generate formtoken - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",1); - ini_set("session.use_strict_mode",0); - session_name("formtoken"); - session_id(session_create_id()); - session_start(); - $formtoken = session_id(); - $_SESSION['formtoken'] = $formtoken; - error_log("generated token: " . $formtoken); - session_write_close(); + $formtoken = hash('sha256', bin2hex(random_bytes(16))); + $cachedToken = $sspCache->getItem($formtoken); + $cachedToken->set($formtoken); + $cachedToken->expiresAfter($cache_form_expiration); + $sspCache->save($cachedToken); + error_log("generated form token: " . $formtoken . " valid for $cache_form_expiration s"); } # Check the entered username for characters that our installation doesn't support @@ -75,6 +71,25 @@ $result = check_username_validity($login,$login_forbidden_chars); } +#============================================================================== +# Check tokenform +#============================================================================== + +if ( !$result ) { + $formtoken = strval($_REQUEST["formtoken"]); + $cachedToken = $sspCache->getItem($formtoken); + if( $cachedToken->get() == $formtoken ) + { + # Remove session + $sspCache->deleteItem($formtoken); + } + else + { + error_log("Invalid form token: sent: $formtoken, stored: " . $cachedToken->get()); + $result = "invalidformtoken"; + } +} + #============================================================================== # Check captcha #============================================================================== @@ -167,51 +182,24 @@ #============================================================================== if ( !$result ) { - # Use PHP session to register token - # We do not generate cookie - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",1); - - session_name("token"); - session_start(); - $_SESSION['login'] = $login; - $_SESSION['time'] = time(); - + # Use cache to register token sent by mail + $token_session_id = hash('sha256', bin2hex(random_bytes(16))); if ( $crypt_tokens ) { - $token = encrypt(session_id(), $keyphrase); + $token = encrypt($token_session_id, $keyphrase); } else { - $token = session_id(); + $token = $token_session_id(); } - session_write_close(); + $cached_token = $sspCache->getItem($token_session_id); + $cached_token->set([ + 'login' => $login, + 'time' => time() + ]); + $cached_token->expiresAfter($cache_token_expiration); + $sspCache->save($cached_token); + error_log("generated cache entry with id: " . $token_session_id. " for storing password reset by mail workflow, valid for $cache_token_expiration s"); } -#============================================================================== -# Check tokenform -#============================================================================== - -if ( !$result ) { - $formtoken = strval($_REQUEST["formtoken"]); - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",1); - ini_set("session.use_strict_mode",0); - session_name("formtoken"); - session_id($formtoken); - session_start(); - if( $_SESSION['formtoken'] == $formtoken ) - { - # Remove session - session_unset(); - session_destroy(); - } - else - { - error_log("Invalid form token: sent: $formtoken, stored: " . $_SESSION['formtoken']); - $result = "invalidformtoken"; - } - session_write_close(); -} - #============================================================================== # Send token by mail #============================================================================== diff --git a/packaging/debian/control b/packaging/debian/control index ce8dd685..794451ae 100644 --- a/packaging/debian/control +++ b/packaging/debian/control @@ -33,4 +33,12 @@ Description: LDAP password change web interface - php-ralouphie-getallheaders = 3.0.3 - php-symfony-deprecation-contracts = 2.5.1 - php-symfony-finder = 7.0.0 - - php-symfony-polyfill = 1.29.0 + - php-symfony-polyfill = v1.31.0 + - php-symfony-deprecation-contracts = v2.5.3 + - php-symfony-var-exporter = v5.4.40 + - php-psr-container = 1.1.2 + - php-symfony-service-contracts = v2.5.3 + - php-psr-cache = 1.0.1 + - php-symfony-cache-contracts = v2.5.3 + - php-psr-log = 1.1.4 + - php-symfony-cache = v5.4.42 diff --git a/packaging/rpm/SPECS/self-service-password.spec b/packaging/rpm/SPECS/self-service-password.spec index bbc6d690..9da4a348 100755 --- a/packaging/rpm/SPECS/self-service-password.spec +++ b/packaging/rpm/SPECS/self-service-password.spec @@ -67,7 +67,15 @@ Provides: bundled(php-psr-http-message) = 2.0 Provides: bundled(php-ralouphie-getallheaders) = 3.0.3 Provides: bundled(php-symfony-deprecation-contracts) = 3.4.0 Provides: bundled(php-symfony-finder) = 7.0.0 -Provides: bundled(php-symfony-polyfill) = 1.29.0 +Provides: bundled(php-symfony-polyfill) = v1.31.0 +Provides: bundled(php-symfony-deprecation-contracts) = v2.5.3 +Provides: bundled(php-symfony-var-exporter) = v5.4.40 +Provides: bundled(php-psr-container) = 1.1.2 +Provides: bundled(php-symfony-service-contracts) = v2.5.3 +Provides: bundled(php-psr-cache) = 1.0.1 +Provides: bundled(php-symfony-cache-contracts) = v2.5.3 +Provides: bundled(php-psr-log) = 1.1.4 +Provides: bundled(php-symfony-cache) = v5.4.42 %description