From 7c661746cf31510f56f5cc5b1ed6b26d431aa581 Mon Sep 17 00:00:00 2001
From: Marc Alexander <admin@m-a-styles.de>
Date: Sun, 28 Apr 2024 20:10:24 +0200
Subject: [PATCH] [ticket/security/276] Add test for expiration timer

SECURITY-276
---
 phpBB/includes/ucp/ucp_resend.php             |  2 +-
 tests/auth/provider_apache_test.php           |  1 +
 tests/functional/user_password_reset_test.php | 84 ++++++++++++++-----
 3 files changed, 67 insertions(+), 20 deletions(-)

diff --git a/phpBB/includes/ucp/ucp_resend.php b/phpBB/includes/ucp/ucp_resend.php
index 0c01838f4e..609ef97b20 100644
--- a/phpBB/includes/ucp/ucp_resend.php
+++ b/phpBB/includes/ucp/ucp_resend.php
@@ -45,7 +45,7 @@ class ucp_resend
 				trigger_error('FORM_INVALID');
 			}
 
-			$sql = 'SELECT user_id, group_id, username, user_email, user_type, user_lang, user_actkey, user_inactive_reason
+			$sql = 'SELECT user_id, group_id, username, user_email, user_type, user_lang, user_actkey, user_actkey_expiration, user_inactive_reason
 				FROM ' . USERS_TABLE . "
 				WHERE user_email = '" . $db->sql_escape($email) . "'
 					AND username_clean = '" . $db->sql_escape(utf8_clean_string($username)) . "'";
diff --git a/tests/auth/provider_apache_test.php b/tests/auth/provider_apache_test.php
index 2dbb1f32a9..df5b53841c 100644
--- a/tests/auth/provider_apache_test.php
+++ b/tests/auth/provider_apache_test.php
@@ -162,6 +162,7 @@ class phpbb_auth_provider_apache_test extends phpbb_database_test_case
 			'user_sig_bbcode_bitfield' => '',
 			'user_jabber' => '',
 			'user_actkey' => '',
+			'user_actkey_expiration' => 0,
 			'user_newpasswd' => '',
 			'user_form_salt' => '',
 			'user_new' => 1,
diff --git a/tests/functional/user_password_reset_test.php b/tests/functional/user_password_reset_test.php
index e912b8ce08..754e4de7d1 100644
--- a/tests/functional/user_password_reset_test.php
+++ b/tests/functional/user_password_reset_test.php
@@ -18,10 +18,14 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 {
 	protected $user_data;
 
+	protected const TEST_USER = 'reset-password-test-user';
+
+	protected const TEST_EMAIL = 'reset-password-test-user@test.com';
+
 	public function test_password_reset()
 	{
 		$this->add_lang('ucp');
-		$user_id = $this->create_user('reset-password-test-user', 'reset-password-test-user@test.com');
+		$user_id = $this->create_user(self::TEST_USER, self::TEST_EMAIL);
 
 		// test without email
 		$crawler = self::request('GET', "ucp.php?mode=sendpassword&sid={$this->sid}");
@@ -41,13 +45,13 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 		// test with correct email
 		$crawler = self::request('GET', "app.php/user/forgot_password?sid={$this->sid}");
 		$form = $crawler->selectButton('submit')->form(array(
-			'email'		=> 'reset-password-test-user@test.com',
+			'email'		=> self::TEST_EMAIL,
 		));
 		$crawler = self::submit($form);
 		$this->assertContainsLang('PASSWORD_RESET_LINK_SENT', $crawler->text());
 
 		// Check if columns in database were updated for password reset
-		$this->get_user_data('reset-password-test-user');
+		$this->get_user_data(self::TEST_USER);
 		$this->assertNotEmpty($this->user_data['reset_token']);
 		$this->assertNotEmpty($this->user_data['reset_token_expiration']);
 		$reset_token = $this->user_data['reset_token'];
@@ -56,31 +60,31 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 		// Check that reset token is only created once per day
 		$crawler = self::request('GET', "app.php/user/forgot_password?sid={$this->sid}");
 		$form = $crawler->selectButton('submit')->form(array(
-			'email'		=> 'reset-password-test-user@test.com',
+			'email'		=> self::TEST_EMAIL,
 		));
 		$crawler = self::submit($form);
 		$this->assertContainsLang('PASSWORD_RESET_LINK_SENT', $crawler->text());
 
-		$this->get_user_data('reset-password-test-user');
+		$this->get_user_data(self::TEST_USER);
 		$this->assertNotEmpty($this->user_data['reset_token']);
 		$this->assertNotEmpty($this->user_data['reset_token_expiration']);
 		$this->assertEquals($reset_token, $this->user_data['reset_token']);
 		$this->assertEquals($reset_token_expiration, $this->user_data['reset_token_expiration']);
 
 		// Create another user with the same email
-		$this->create_user('reset-password-test-user1', 'reset-password-test-user@test.com');
+		$this->create_user('reset-password-test-user1', self::TEST_EMAIL);
 
 		// Test that username is now also required
 		$crawler = self::request('GET', "app.php/user/forgot_password?sid={$this->sid}");
 		$form = $crawler->selectButton('submit')->form(array(
-			'email'		=> 'reset-password-test-user@test.com',
+			'email'		=> self::TEST_EMAIL,
 		));
 		$crawler = self::submit($form);
 		$this->assertContainsLang('EMAIL_NOT_UNIQUE', $crawler->text());
 
 		// Provide both username and email
 		$form = $crawler->selectButton('submit')->form(array(
-			'email'		=> 'reset-password-test-user@test.com',
+			'email'		=> self::TEST_EMAIL,
 			'username'	=> 'reset-password-test-user1',
 		));
 		$crawler = self::submit($form);
@@ -95,7 +99,7 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 
 	public function test_login_after_reset()
 	{
-		$this->login('reset-password-test-user');
+		$this->login(self::TEST_USER);
 	}
 
 	public function data_reset_user_password()
@@ -117,7 +121,7 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 	public function test_reset_user_password($expected, $user_id, $token)
 	{
 		$this->add_lang('ucp');
-		$this->get_user_data('reset-password-test-user');
+		$this->get_user_data(self::TEST_USER);
 		$user_id = !$user_id ? $this->user_data['user_id'] : $user_id;
 		$token = !$token ? $this->user_data['reset_token'] : $token;
 
@@ -131,8 +135,8 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 		{
 			$form = $crawler->filter('input[type=submit]')->form();
 			$values = array_merge($form->getValues(), [
-				'new_password'			=> 'reset-password-test-user',
-				'new_password_confirm'	=> 'reset-password-test-user',
+				'new_password'			=> self::TEST_USER,
+				'new_password_confirm'	=> self::TEST_USER,
 			]);
 			$crawler = self::submit($form, $values);
 			$this->assertContainsLang('PASSWORD_RESET', $crawler->text());
@@ -146,7 +150,7 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 		$this->assertStringContainsString($this->lang('LOGIN_EXPLAIN_UCP'), $crawler->filter('html')->text());
 
 		$form = $crawler->selectButton($this->lang('LOGIN'))->form();
-		$crawler = self::submit($form, array('username' => 'reset-password-test-user', 'password' => 'reset-password-test-user'));
+		$crawler = self::submit($form, array('username' => self::TEST_USER, 'password' => self::TEST_USER));
 		$this->assertStringNotContainsString($this->lang('LOGIN'), $crawler->filter('.navbar')->text());
 
 		$cookies = self::$cookieJar->all();
@@ -167,17 +171,17 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 
 		$form = $crawler->selectButton($this->lang('LOGIN'))->form();
 		// Try logging in with the old password
-		$crawler = self::submit($form, array('username' => 'reset-password-test-user', 'password' => 'reset-password-test-userreset-password-test-user'));
+		$crawler = self::submit($form, array('username' => self::TEST_USER, 'password' => 'reset-password-test-userreset-password-test-user'));
 		$this->assertStringContainsString($this->lang('LOGIN_ERROR_PASSWORD', '', ''), $crawler->filter('html')->text());
 	}
 
 	/**
 	 * @depends test_login
 	 */
-	public function test_acivateAfterDeactivate()
+	public function test_activateAfterDeactivate()
 	{
 		// User is active, actkey should not exist
-		$this->get_user_data('reset-password-test-user');
+		$this->get_user_data(self::TEST_USER);
 		$this->assertEmpty($this->user_data['user_actkey']);
 
 		$this->login();
@@ -189,7 +193,7 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 		$this->assertContainsLang('FIND_USERNAME', $crawler->filter('html')->text());
 
 		$form = $crawler->selectButton('Submit')->form();
-		$crawler = self::submit($form, array('username' => 'reset-password-test-user'));
+		$crawler = self::submit($form, array('username' => self::TEST_USER));
 
 		// Deactivate account and go back to overview of current user
 		$this->assertContainsLang('USER_TOOLS', $crawler->filter('html')->text());
@@ -201,7 +205,7 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 		$crawler = self::request('GET', preg_replace('#(.+)(adm/index.php.+)#', '$2', $link->getUri()));
 
 		// Ensure again that actkey is empty after deactivation
-		$this->get_user_data('reset-password-test-user');
+		$this->get_user_data(self::TEST_USER);
 		$this->assertEmpty($this->user_data['user_actkey']);
 
 		// Force reactivation of account and check that act key is not empty anymore
@@ -210,8 +214,50 @@ class phpbb_functional_user_password_reset_test extends phpbb_functional_test_ca
 		$crawler = self::submit($form, array('action' => 'reactivate'));
 		$this->assertContainsLang('FORCE_REACTIVATION_SUCCESS', $crawler->filter('html')->text());
 
-		$this->get_user_data('reset-password-test-user');
+		$this->get_user_data(self::TEST_USER);
 		$this->assertNotEmpty($this->user_data['user_actkey']);
+
+		// Logout and try resending activation email, account is deactivated though
+		$this->logout();
+		$this->add_lang('ucp');
+
+		$crawler = self::request('GET', 'ucp.php?mode=resend_act');
+		$this->assertContainsLang('UCP_RESEND', $crawler->filter('html')->text());
+		$form = $crawler->filter('input[name=submit]')->selectButton('Submit')->form();
+		$crawler = self::submit($form, [
+			'username'		=> self::TEST_USER,
+			'email'			=> self::TEST_EMAIL,
+		]);
+		$this->assertContainsLang('ACCOUNT_DEACTIVATED', $crawler->filter('html')->text());
+	}
+
+	/**
+	 * @depends test_activateAfterDeactivate
+	 */
+	public function test_resendActivation()
+	{
+		// User is deactivated and should have actkey, actkey should not exist
+		$this->get_user_data(self::TEST_USER);
+		$this->assertNotEmpty($this->user_data['user_actkey']);
+
+		// Change reason for inactivity
+		$db = $this->get_db();
+
+		$sql = 'UPDATE ' . USERS_TABLE . '
+			SET user_inactive_reason = ' . INACTIVE_REMIND . '
+			WHERE user_id = ' . (int) $this->user_data['user_id'];
+		$db->sql_query($sql);
+
+		$this->add_lang('ucp');
+
+		$crawler = self::request('GET', 'ucp.php?mode=resend_act');
+		$this->assertContainsLang('UCP_RESEND', $crawler->filter('html')->text());
+		$form = $crawler->filter('input[name=submit]')->selectButton('Submit')->form();
+		$crawler = self::submit($form, [
+			'username'		=> self::TEST_USER,
+			'email'			=> self::TEST_EMAIL,
+		]);
+		$this->assertContainsLang('ACTIVATION_ALREADY_SENT', $crawler->filter('html')->text());
 	}
 
 	protected function get_user_data($username)