Procházet zdrojové kódy

Make the login challenge nonce one-time (CWE-294) (#9334)

* auth: make the login challenge nonce one-time

The per-login challenge nonce stored in the session was never consumed or
cleared, so a captured challenge stayed replayable and survived logout
(CWE-294). The client (p/scripts/extra.js) already fetches a fresh nonce for
every attempt, so consuming it does not affect legitimate retries.

Consume the nonce (unset it) as soon as it is read for verification in form
login, reauthentication, password change and account self-deletion, and clear
it in FreshRSS_Auth::removeAccess so logout invalidates any outstanding
challenge.

* auth: don't clear the nonce in removeAccess (breaks login)

removeAccess() runs on every unauthenticated request via FreshRSS_Auth::init()
-- including the login POST itself, before formLoginAction reads the nonce -- so
clearing it there rejected every login with 'Invalid session'. Thanks
@jamalkamaladdin for the measured catch. The one-time guarantee is preserved by
consuming the nonce when it is read for verification (login/reauth/password
change/self-deletion); logout no longer needs a separate clear.
Gigi před 13 hodinami
rodič
revize
d5ad610c3d

+ 2 - 0
app/Controllers/authController.php

@@ -118,6 +118,7 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 
 		if ($isPOST) {
 			$nonce = Minz_Session::paramString('nonce');
+			Minz_Session::_param('nonce', false);	// One-time: consume the challenge so it cannot be replayed.
 			$username = Minz_Request::paramString('username');
 			$challenge = Minz_Request::paramString('challenge');
 			$ip_address = Minz_Request::connectionRemoteAddress();
@@ -215,6 +216,7 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 		if (Minz_Request::isPost()) {
 			$username = Minz_User::name() ?? '';
 			$nonce = Minz_Session::paramString('nonce');
+			Minz_Session::_param('nonce', false);	// One-time: consume the challenge so it cannot be replayed.
 			$challenge = Minz_Request::paramString('challenge');
 			if (!FreshRSS_FormAuth::checkCredentials(
 				$username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge

+ 2 - 0
app/Controllers/userController.php

@@ -174,6 +174,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			if ($challenge !== '') {
 				$username = Minz_User::name();
 				$nonce = Minz_Session::paramString('nonce');
+				Minz_Session::_param('nonce', false);	// One-time: consume the challenge so it cannot be replayed.
 
 				$newPasswordPlain = Minz_Request::paramString('newPasswordPlain', plaintext: true);
 				$confirmPasswordPlain = Minz_Request::paramString('confirmPasswordPlain', plaintext: true);
@@ -701,6 +702,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			if ($self_deletion) {
 				// We check the password if it’s a self-destruction
 				$nonce = Minz_Session::paramString('nonce');
+				Minz_Session::_param('nonce', false);	// One-time: consume the challenge so it cannot be replayed.
 				$challenge = Minz_Request::paramString('challenge');
 
 				$ok &= FreshRSS_FormAuth::checkCredentials(