CVE-2022-0196: Cross-Site Request Forgery (CSRF) (CWE-352)
value: phoronix-test-suite is vulnerable to Cross-Site Request Forgery (CSRF)
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2022-01-13T01:15Z
Repository: https://github.com/phoronix-test-suite/phoronix-test-suite
Language: PHP
Files changed: pts-core/objects/pts_strings.php, pts-core/phoromatic/pages/phoromatic_local_suites.php, pts-core/phoromatic/pages/phoromatic_schedules.php, pts-core/phoromatic/pages/phoromatic_users.php, pts-core/phoromatic/pages/phoromatic_welcome.php, pts-core/phoromatic/phoromatic_functions.php, pts-core/phoromatic/public_html/event.php, pts-core/pts-core.php
Commit date: 2022-01-11 18:45:29 -0600
Commit message: phoromatic: Some additional input sanitization and starting on some CSRF token handling

Diff: diff --git a/pts-core/objects/pts_strings.php b/pts-core/objects/pts_strings.php
index a0c562424..c9b457b35 100644
--- a/pts-core/objects/pts_strings.php
+++ b/pts-core/objects/pts_strings.php
@@ -783,10 +783,10 @@ class pts_strings
 	{
 		return htmlspecialchars($input, ENT_NOQUOTES, 'UTF-8');
 	}
 	public static function simple($input)
 	{
-		return empty($str) ? '' : pts_strings::keep_in_string($input, pts_strings::CHAR_LETTER | pts_strings::CHAR_NUMERIC | pts_strings::CHAR_DASH | pts_strings::CHAR_DECIMAL | pts_strings::CHAR_SPACE | pts_strings::CHAR_UNDERSCORE | pts_strings::CHAR_COMMA | pts_strings::CHAR_AT | pts_strings::CHAR_COLON);
+		return empty($input) ? '' : pts_strings::keep_in_string($input, pts_strings::CHAR_LETTER | pts_strings::CHAR_NUMERIC | pts_strings::CHAR_DASH | pts_strings::CHAR_DECIMAL | pts_strings::CHAR_SPACE | pts_strings::CHAR_UNDERSCORE | pts_strings::CHAR_COMMA | pts_strings::CHAR_AT | pts_strings::CHAR_COLON);
 	}
 }
 
 ?>
diff --git a/pts-core/phoromatic/pages/phoromatic_local_suites.php b/pts-core/phoromatic/pages/phoromatic_local_suites.php
index 9ec19f6ef..d2fe400d8 100644
--- a/pts-core/phoromatic/pages/phoromatic_local_suites.php
+++ b/pts-core/phoromatic/pages/phoromatic_local_suites.php
@@ -37,11 +37,11 @@ class phoromatic_local_suites implements pts_webui_interface
 	public static function render_page_process($PATH)
 	{
 		$suite_dir = phoromatic_server::phoromatic_account_suite_path($_SESSION['AccountID']);
 		$main = '<h1>Local Suites</h1><p>These are test suites created by you or another account within your group. Suites are an easy collection of test profiles. New suits can be trivially made via the <a href="/?build_suite">build suite</a> page.</p>';
 
-		if(!PHOROMATIC_USER_IS_VIEWER && isset($PATH[0]) && $PATH[0] == 'delete')
+		if(!PHOROMATIC_USER_IS_VIEWER && isset($PATH[0]) && $PATH[0] == 'delete' && verify_submission_token())
 		{
 			foreach(explode(',', $PATH[1]) as $id)
 			{
 				if(is_file($suite_dir . $id . '/suite-definition.xml'))
 				{
@@ -62,11 +62,11 @@ class phoromatic_local_suites implements pts_webui_interface
 			$main .= '<a name="' . $id . '"></a><h1>' . pts_strings::sanitize($test_suite->get_title()) . ' [' . $id . ']</h1>';
 			$main .= '<p><strong>' . pts_strings::sanitize($test_suite->get_maintainer()) . '</strong></p>';
 			$main .= '<p><em>' . pts_strings::sanitize($test_suite->get_description()) . '</em></p>';
 			if(!PHOROMATIC_USER_IS_VIEWER)
 			{
-				$main .= '<p><a href="?build_suite/' . $id . '">Edit Suite</a> - <a href="?local_suites/delete/' . $id . '">Delete Suite</a></p>';
+				$main .= '<p><a href="?build_suite/' . $id . '">Edit Suite</a> - <a href="?local_suites/delete/' . $id . append_token_to_url() . '">Delete Suite</a></p>';
 			}
 			$main .= '<div style="max-height: 400px; width: 80%; overflow-y: scroll;">';
 			$test_suite->sort_contained_tests();
 			foreach($test_suite->get_contained_test_result_objects() as $tro)
 			{
diff --git a/pts-core/phoromatic/pages/phoromatic_schedules.php b/pts-core/phoromatic/pages/phoromatic_schedules.php
index 8e0cd7863..9424a6557 100644
--- a/pts-core/phoromatic/pages/phoromatic_schedules.php
+++ b/pts-core/phoromatic/pages/phoromatic_schedules.php
@@ -121,20 +121,20 @@ class phoromatic_schedules implements pts_webui_interface
 						phoromatic_add_activity_stream_event('tests_for_schedule', $to_remove[0] . ' - ' . $to_remove[1], 'removed');
 					}
 					else if(isset($PATH[1]) && $PATH[1] == 'delete-trigger' && !empty($PATH[2]))
 					{
 						// REMOVE TRIGGER
-						$trigger = base64_decode($PATH[2]);
+						$trigger = pts_strings::sanitize(base64_decode($PATH[2]));
 						$stmt = phoromatic_server::$db->prepare('DELETE FROM phoromatic_schedules_triggers WHERE AccountID = :account_id AND Trigger = :trigger AND ScheduleID = :schedule_id');
 						$stmt->bindValue(':account_id', $_SESSION['AccountID']);
 						$stmt->bindValue(':schedule_id', $PATH[0]);
 						$stmt->bindValue(':trigger', $trigger);
 						$result = $stmt->execute();
 						if($result)
 							$main .= '<h2 style="color: red;">Trigger Removed: ' . $trigger . '</h2>';
 					}
-					else if(isset($PATH[1]) && in_array($PATH[1], array('activate', 'deactivate')))
+					else if(isset($PATH[1]) && in_array($PATH[1], array('activate', 'deactivate')) && verify_submission_token())
 					{
 						switch($PATH[1])
 						{
 							case 'deactivate':
 								$new_state = 0;
@@ -199,15 +199,15 @@ class phoromatic_schedules implements pts_webui_interface
 				{
 					$main .= '<p><a href="?sched/' . $PATH[0] . '">Edit Schedule</a> | ';
 
 					if($row['State'] == 1)
 					{
-						$main .= '<a href="?schedules/' . $PATH[0] . '/deactivate">Deactivate Schedule</a>';
+						$main .= '<a href="?schedules/' . $PATH[0] . '/deactivate' . append_token_to_url()  . '">Deactivate Schedule</a>';
 					}
 					else
 					{
-						$main .= '<a href="?schedules/' . $PATH[0] . '/activate">Activate Schedule</a>';
+						$main .= '<a href="?schedules/' . $PATH[0] . '/activate' . append_token_to_url()  . '">Activate Schedule</a>';
 					}
 
 					$main .= '</p>';
 				}
 
diff --git a/pts-core/phoromatic/pages/phoromatic_users.php b/pts-core/phoromatic/pages/phoromatic_users.php
index 578585122..ee84106a9 100644
--- a/pts-core/phoromatic/pages/phoromatic_users.php
+++ b/pts-core/phoromatic/pages/phoromatic_users.php
@@ -40,11 +40,11 @@ class phoromatic_users implements pts_webui_interface
 		{
 			echo phoromatic_error_page('Unauthorized Access', 'You aren\'t an account administrator!');
 			return;
 		}
 
-		if(isset($_POST['group_name']))
+		if(isset($_POST['group_name']) && verify_submission_token())
 		{
 			phoromatic_quit_if_invalid_input_found(array('group_name'));
 			$stmt = phoromatic_server::$db->prepare('UPDATE phoromatic_accounts SET GroupName = :group_name WHERE AccountID = :account_id');
 			$stmt->bindValue(':group_name', pts_strings::simple($_POST['group_name']));
 			$stmt->bindValue(':account_id', $_SESSION['AccountID']);
@@ -240,11 +240,11 @@ class phoromatic_users implements pts_webui_interface
 			</select></p>
 			<p><input name="submit" value="Add User" type="submit" /></p>
 			</form>';
 
 		$group_name = phoromatic_server::account_id_to_group_name($_SESSION['AccountID']);
-		$main .= '<hr /><form action="' . $_SERVER['REQUEST_URI'] . '" name="group_name" id="group_name" method="post"><h2>Group Name</h2>
+		$main .= '<hr /><form action="' . $_SERVER['REQUEST_URI'] . '" name="group_name" id="group_name" method="post"><h2>Group Name</h2>' . write_token_in_form() . '
 			<p>A group name is an alternative, user-facing name for this set of accounts. The group name feature is primarily useful for being able to better distinguish results between groups when sharing of data within a large organization, etc. The group name is showed next to test results when viewing results from multiple groups/accounts.</p>
 			<h3>Group Name</h3>
 			<p><input type="text" name="group_name" value="' . $group_name . '" /></p>
 			<p><input name="submit" value="Update Group Name" type="submit" /></p>
 			</form>';
diff --git a/pts-core/phoromatic/pages/phoromatic_welcome.php b/pts-core/phoromatic/pages/phoromatic_welcome.php
index 2fd251df9..a1e297cd1 100644
--- a/pts-core/phoromatic/pages/phoromatic_welcome.php
+++ b/pts-core/phoromatic/pages/phoromatic_welcome.php
@@ -128,10 +128,11 @@ class phoromatic_welcome implements pts_webui_interface
 					$_SESSION['UserID'] = $user_id;
 					$_SESSION['UserName'] = $user;
 					$_SESSION['AccountID'] = $account_id;
 					$_SESSION['AdminLevel'] = $admin_level;
 					$_SESSION['CreatedOn'] = $created_on;
+					$_SESSION['Token'] = sha1($account_salt . (function_exists('random_bytes') ? bin2hex(random_bytes(32)) : rand()) . PTS_CORE_VERSION . time());
 					$_SESSION['CoreVersionOnSignOn'] = PTS_CORE_VERSION;
 					$account_salt = phoromatic_server::$db->exec('UPDATE phoromatic_users SET LastIP = \'' . $_SERVER['REMOTE_ADDR'] . '\', LastLogin = \'' . phoromatic_server::current_time() . '\' WHERE UserName = "' . $matching_user['UserName'] . '"');
 					session_write_close();
 
 					pts_file_io::mkdir(phoromatic_server::phoromatic_account_path($account_id));
diff --git a/pts-core/phoromatic/phoromatic_functions.php b/pts-core/phoromatic/phoromatic_functions.php
index 9fb7f0786..45820488b 100644
--- a/pts-core/phoromatic/phoromatic_functions.php
+++ b/pts-core/phoromatic/phoromatic_functions.php
@@ -521,10 +521,22 @@ function phoromatic_oldest_result_for_schedule($schedule_id)
 		$old_time[$schedule_id] = $row['UploadTime'];
 	}
 
 	return $old_time[$schedule_id];
 }
+function write_token_in_form()
+{
+	return '<input type="hidden" name="token_submit" value="' . $_SESSION['Token'] . '" />';
+}
+function append_token_to_url()
+{
+	return '/&token_submit=' . $_SESSION['Token'];
+}
+function verify_submission_token()
+{
+	return isset($_REQUEST['token_submit']) && $_REQUEST['token_submit'] == $_SESSION['Token'];
+}
 function create_new_phoromatic_account($register_username, $register_password, $register_password_confirm, $register_email, $seed_accountid = null)
 {
 	// REGISTER NEW USER
 	if(strlen($register_username) < 4 || strpos($register_username, ' ') !== false)
 	{
diff --git a/pts-core/phoromatic/public_html/event.php b/pts-core/phoromatic/public_html/event.php
index 71a96463e..7a87ca6f3 100644
--- a/pts-core/phoromatic/public_html/event.php
+++ b/pts-core/phoromatic/public_html/event.php
@@ -83,19 +83,20 @@ switch($_GET['type'])
 				$sys_row = $result->fetchArray();
 				$sub_target = $sys_row['SystemID'];
 			}
 		}
 
+		$trigger = pts_strings::sanitize($_GET['trigger']);
 		$stmt = phoromatic_server::$db->prepare('INSERT INTO phoromatic_schedules_triggers (AccountID, ScheduleID, Trigger, TriggeredOn, SubTarget) VALUES (:account_id, :schedule_id, :trigger, :triggered_on, :sub_target)');
 		$stmt->bindValue(':account_id',	$user_row['AccountID']);
 		$stmt->bindValue(':schedule_id', $schedule_row['ScheduleID']);
-		$stmt->bindValue(':trigger', $_GET['trigger']);
+		$stmt->bindValue(':trigger', $trigger);
 		$stmt->bindValue(':triggered_on', phoromatic_server::current_time());
 		$stmt->bindValue(':sub_target', $sub_target);
 		if($stmt->execute())
 		{
-			echo 'Trigger ' . htmlspecialchars($_GET['trigger']) . ' added!';
+			echo 'Trigger ' . $trigger . ' added!';
 		}
 		break;
 
 }
 ?>
diff --git a/pts-core/pts-core.php b/pts-core/pts-core.php
index 21f3fe68e..24f8df0c4 100644
--- a/pts-core/pts-core.php
+++ b/pts-core/pts-core.php
@@ -227,11 +227,11 @@ function pts_needed_extensions()
 		array(0, function_exists('readline'), 'Readline', 'Readline support is useful for tab-based auto-completion support.'),
 		);
 }
 
 pts_define('PTS_VERSION', '10.8.0');
-pts_define('PTS_CORE_VERSION', 10800);
+pts_define('PTS_CORE_VERSION', 10801);
 pts_define('PTS_RELEASE_DATE', '2021125');
 pts_define('PTS_CODENAME', 'Nesseby');
 
 pts_define('PTS_IS_CLIENT', (defined('PTS_MODE') && strstr(PTS_MODE, 'CLIENT') !== false));
 pts_define('PTS_IS_WEB_CLIENT', (defined('PTS_MODE') && PTS_MODE == 'WEB_CLIENT'));

Vulnerable code: 		return empty($str) ? '' : pts_strings::keep_in_string($input, pts_strings::CHAR_LETTER | pts_strings::CHAR_NUMERIC | pts_strings::CHAR_DASH | pts_strings::CHAR_DECIMAL | pts_strings::CHAR_SPACE | pts_strings::CHAR_UNDERSCORE | pts_strings::CHAR_COMMA | pts_strings::CHAR_AT | pts_strings::CHAR_COLON);
		if(!PHOROMATIC_USER_IS_VIEWER && isset($PATH[0]) && $PATH[0] == 'delete')
				$main .= '<p><a href="?build_suite/' . $id . '">Edit Suite</a> - <a href="?local_suites/delete/' . $id . '">Delete Suite</a></p>';
						$trigger = base64_decode($PATH[2]);
					else if(isset($PATH[1]) && in_array($PATH[1], array('activate', 'deactivate')))
						$main .= '<a href="?schedules/' . $PATH[0] . '/deactivate">Deactivate Schedule</a>';
						$main .= '<a href="?schedules/' . $PATH[0] . '/activate">Activate Schedule</a>';
		if(isset($_POST['group_name']))
		$main .= '<hr /><form action="' . $_SERVER['REQUEST_URI'] . '" name="group_name" id="group_name" method="post"><h2>Group Name</h2>
		$stmt->bindValue(':trigger', $_GET['trigger']);
			echo 'Trigger ' . htmlspecialchars($_GET['trigger']) . ' added!';
pts_define('PTS_CORE_VERSION', 10800);
Fixed code: 		return empty($input) ? '' : pts_strings::keep_in_string($input, pts_strings::CHAR_LETTER | pts_strings::CHAR_NUMERIC | pts_strings::CHAR_DASH | pts_strings::CHAR_DECIMAL | pts_strings::CHAR_SPACE | pts_strings::CHAR_UNDERSCORE | pts_strings::CHAR_COMMA | pts_strings::CHAR_AT | pts_strings::CHAR_COLON);
		if(!PHOROMATIC_USER_IS_VIEWER && isset($PATH[0]) && $PATH[0] == 'delete' && verify_submission_token())
				$main .= '<p><a href="?build_suite/' . $id . '">Edit Suite</a> - <a href="?local_suites/delete/' . $id . append_token_to_url() . '">Delete Suite</a></p>';
						$trigger = pts_strings::sanitize(base64_decode($PATH[2]));
					else if(isset($PATH[1]) && in_array($PATH[1], array('activate', 'deactivate')) && verify_submission_token())
						$main .= '<a href="?schedules/' . $PATH[0] . '/deactivate' . append_token_to_url()  . '">Deactivate Schedule</a>';
						$main .= '<a href="?schedules/' . $PATH[0] . '/activate' . append_token_to_url()  . '">Activate Schedule</a>';
		if(isset($_POST['group_name']) && verify_submission_token())
		$main .= '<hr /><form action="' . $_SERVER['REQUEST_URI'] . '" name="group_name" id="group_name" method="post"><h2>Group Name</h2>' . write_token_in_form() . '
					$_SESSION['Token'] = sha1($account_salt . (function_exists('random_bytes') ? bin2hex(random_bytes(32)) : rand()) . PTS_CORE_VERSION . time());
function write_token_in_form()
{
	return '<input type="hidden" name="token_submit" value="' . $_SESSION['Token'] . '" />';
}
function append_token_to_url()
{
	return '/&token_submit=' . $_SESSION['Token'];
}
function verify_submission_token()
{
	return isset($_REQUEST['token_submit']) && $_REQUEST['token_submit'] == $_SESSION['Token'];
}
		$trigger = pts_strings::sanitize($_GET['trigger']);
		$stmt->bindValue(':trigger', $trigger);
			echo 'Trigger ' . $trigger . ' added!';
pts_define('PTS_CORE_VERSION', 10801);