CVE-2023-28854: Improper Neutralization of Special Elements used in a Command ('Command Injection') (CWE-77)
value: nophp is a PHP web framework. Prior to version 0.0.1, nophp is vulnerable to shell command injection on httpd user. A patch was made available at commit e5409aa2d441789cbb35f6b119bef97ecc3986aa on 2023-03-30. Users should update index.php to 2023-03-30 or later or, as a workaround, add a function such as `env_patchsample230330.php` to env.php.
CWE description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
CVSS v3 base score: 8.8
Published: 2023-04-03T18:15Z
Repository: https://github.com/paijp/nophp
Language: PHP
Files changed: README.md, env_patchsample230330a.php, index.php
Commit date: 2023-03-30 13:05:12 +0900
Commit message: Security fix: command injection when send a password setting mail.

Diff: diff --git a/README.md b/README.md
index 38e96fa..1d38435 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,11 @@
 - Powerful log for debug.
 - Javascript not required.
 
 ## Important changes.
 
+- 2023/03/30 **Important security fix**. Please see env_patchsample230330a.php .
 - 2023/03/05 The argument '$par' in recordholder::__construct() is no longer parsed with parsewithbq(). Please parse argument in constructor in user class.
 - 2022/07/11 Return value of commandparser::parsehtml() is discarded. Please call commandparser::output() for output.
 - 2022/07/11 debuglog directory changed to 'debuglog???????*'. Please create a directory with at least 7 characters that is hard to guess after 'debuglog'.
 
 ## How to use.
diff --git a/env_patchsample230330a.php b/env_patchsample230330a.php
new file mode 100644
index 0000000..3fa2119
--- /dev/null
+++ b/env_patchsample230330a.php
@@ -0,0 +1,133 @@
+<?php
+
+#
+# ======== 2023/03/30: Security patch sample. ======== 
+#
+# What is this security risk type?
+#	- Shell command injection on httpd user.
+#
+# At what point does this security risk become apparent?
+#	- Send password setting mail or mail-login mail.
+#	- Maybe many system have a 'forget password?' link.
+#
+# Who can use this security risk?
+#	- Users who can add or modify the login field in login table to invalid mail address.
+#	- In many system, admin user will be able to create or modify a user.
+#	- If your system allow users to change login name to invalid mail address, the user can use this security risk. 
+#
+# How to fix it?
+#	- Please use the newest index.php or add this bq_login() function to your env.php.
+#
+# Important line is here:
+# <			if (!preg_match('/^[0-9A-Za-z][-_.@+0-9A-Za-z]*$/', $login))
+# <				log_die("invalid character in login.");
+#
+
+function	bq_login($type = null, $r = null)
+{
+	global	$tablelist;
+	global	$sys;
+	
+	if ($type === null)
+		return array();
+	switch ($type) {
+		case	"emptypass":		# empty password
+			header("Location: {$sys->url}?mode=2mailsent");
+			if (($mailcmd = $sys->mailcmd) == "")
+				log_die("mailcmd empty.");
+			if (($mailbody = @$sys->mailbody) == "")
+				log_die("mailbody empty.");
+			if (@$sys->mailinterval <= 0)
+				log_die("mailinterval <= 0.");
+	
+			if (($login = @$_POST["login"]) == "")
+				log_die("login empty.");
+			if (!preg_match('/^[0-9A-Za-z][-_.@+0-9A-Za-z]*$/', $login))
+				log_die("invalid character in login.");
+			$list = $tablelist["login"]->getrecordidlist("where login = ?", array($login));
+			if (count($list) < 1)
+				log_die("no login found");
+			$r = $tablelist["login"]->getrecord($uid = $list[0]);
+	
+			if (@$r->v_mailsent > $sys->now - $sys->mailinterval)
+				log_die("mail interval too short.");
+	
+			$key =$r->getrandom();
+			$r->setmailkey($key);
+	
+			$a0 = array("@addr@", "@url@");
+			$a1 = array($login, "{$sys->url}?mode=1login&uid={$uid}&key={$key}&");
+			$mailcmd = str_replace($a0, $a1, $mailcmd);
+			$mailbody = str_replace($a0, $a1, $mailbody);
+			if (($fp = popen($mailcmd, "w")) === null)
+				log_die("popen failed: {$mailcmd}");
+			fputs($fp, $mailbody);
+			pclose($fp);
+			log_die("mail sent.");
+		case	"goodlogin":		# login success
+			break;
+		case	"badlogin":		# login fail
+			break;
+		case	"logout":		# logout
+			break;
+		case	"changelogin":	# change mail address
+			break;
+		case	"changepass":		# change password
+			break;
+	}
+	return array();
+}
+
+
+#
+# ======== Original env.php ========
+#
+
+
+date_default_timezone_set("Asia/Tokyo");
+putenv("LANG=ja_JP.UTF-8");		# for sendmail.php
+
+#
+# In first time, this user will be created.
+#
+
+$sys->defaultuser = "nobody@something.com";
+
+## If you cant use mail(for setting password), use this.
+# $sys->defaultpass = "sample";
+
+#
+# Filesystem settings.
+#
+
+$sys->htmlbase = "./res/";
+$sys->sqlpath = "sqlite:/var/www/db/nophp.sq3";
+# $sys->sqlpath = "mysql:dbname=test";		# sorry, not tested.
+
+$sys->rootpage = "g9999";
+
+#
+# If you want to use 'index.php?mode=sql', you have to set this basic authentication.
+# $sys->auth_pass = sha1( $sys->auth_salt . $pass );
+#
+
+$sys->auth_user = "admin";
+$sys->auth_salt = "235ba225813f156690b798dc978099e1e76441ee";
+# $sys->auth_pass = "";
+
+#
+# If the password on the login form is empty, a mail with URL will be sent.
+#
+
+$sys->mailcmd = "mail -s 'login URL.' @addr@";
+$sys->mailbody = "@url@";
+
+#
+# You can send a report to slack with debuglog URL.
+#
+
+# $sys->reportjsonurl = "https://hooks.slack.com/services/9999";
+$sys->reportjsonbase = array("text" => "@body@\n<@link@>");
+
+
+?>
diff --git a/index.php b/index.php
index ab91565..e247d8e 100644
--- a/index.php
+++ b/index.php
@@ -3681,10 +3681,12 @@ if (!function_exists("bq_login")) {
 				if (@$sys->mailinterval <= 0)
 					log_die("mailinterval <= 0.");
 		
 				if (($login = @$_POST["login"]) == "")
 					log_die("login empty.");
+				if (!preg_match('/^[0-9A-Za-z][-_.@+0-9A-Za-z]*$/', $login))
+					log_die("invalid character in login.");
 				$list = $tablelist["login"]->getrecordidlist("where login = ?", array($login));
 				if (count($list) < 1)
 					log_die("no login found");
 				$r = $tablelist["login"]->getrecord($uid = $list[0]);
 		

Fixed code: - 2023/03/30 **Important security fix**. Please see env_patchsample230330a.php .
<?php

#
# ======== 2023/03/30: Security patch sample. ======== 
#
# What is this security risk type?
#	- Shell command injection on httpd user.
#
# At what point does this security risk become apparent?
#	- Send password setting mail or mail-login mail.
#	- Maybe many system have a 'forget password?' link.
#
# Who can use this security risk?
#	- Users who can add or modify the login field in login table to invalid mail address.
#	- In many system, admin user will be able to create or modify a user.
#	- If your system allow users to change login name to invalid mail address, the user can use this security risk. 
#
# How to fix it?
#	- Please use the newest index.php or add this bq_login() function to your env.php.
#
# Important line is here:
# <			if (!preg_match('/^[0-9A-Za-z][-_.@+0-9A-Za-z]*$/', $login))
# <				log_die("invalid character in login.");
#

function	bq_login($type = null, $r = null)
{
	global	$tablelist;
	global	$sys;
	
	if ($type === null)
		return array();
	switch ($type) {
		case	"emptypass":		# empty password
			header("Location: {$sys->url}?mode=2mailsent");
			if (($mailcmd = $sys->mailcmd) == "")
				log_die("mailcmd empty.");
			if (($mailbody = @$sys->mailbody) == "")
				log_die("mailbody empty.");
			if (@$sys->mailinterval <= 0)
				log_die("mailinterval <= 0.");
	
			if (($login = @$_POST["login"]) == "")
				log_die("login empty.");
			if (!preg_match('/^[0-9A-Za-z][-_.@+0-9A-Za-z]*$/', $login))
				log_die("invalid character in login.");
			$list = $tablelist["login"]->getrecordidlist("where login = ?", array($login));
			if (count($list) < 1)
				log_die("no login found");
			$r = $tablelist["login"]->getrecord($uid = $list[0]);
	
			if (@$r->v_mailsent > $sys->now - $sys->mailinterval)
				log_die("mail interval too short.");
	
			$key =$r->getrandom();
			$r->setmailkey($key);
	
			$a0 = array("@addr@", "@url@");
			$a1 = array($login, "{$sys->url}?mode=1login&uid={$uid}&key={$key}&");
			$mailcmd = str_replace($a0, $a1, $mailcmd);
			$mailbody = str_replace($a0, $a1, $mailbody);
			if (($fp = popen($mailcmd, "w")) === null)
				log_die("popen failed: {$mailcmd}");
			fputs($fp, $mailbody);
			pclose($fp);
			log_die("mail sent.");
		case	"goodlogin":		# login success
			break;
		case	"badlogin":		# login fail
			break;
		case	"logout":		# logout
			break;
		case	"changelogin":	# change mail address
			break;
		case	"changepass":		# change password
			break;
	}
	return array();
}


#
# ======== Original env.php ========
#


date_default_timezone_set("Asia/Tokyo");
putenv("LANG=ja_JP.UTF-8");		# for sendmail.php

#
# In first time, this user will be created.
#

$sys->defaultuser = "nobody@something.com";

## If you cant use mail(for setting password), use this.
# $sys->defaultpass = "sample";

#
# Filesystem settings.
#

$sys->htmlbase = "./res/";
$sys->sqlpath = "sqlite:/var/www/db/nophp.sq3";
# $sys->sqlpath = "mysql:dbname=test";		# sorry, not tested.

$sys->rootpage = "g9999";

#
# If you want to use 'index.php?mode=sql', you have to set this basic authentication.
# $sys->auth_pass = sha1( $sys->auth_salt . $pass );
#

$sys->auth_user = "admin";
$sys->auth_salt = "235ba225813f156690b798dc978099e1e76441ee";
# $sys->auth_pass = "";

#
# If the password on the login form is empty, a mail with URL will be sent.
#

$sys->mailcmd = "mail -s 'login URL.' @addr@";
$sys->mailbody = "@url@";

#
# You can send a report to slack with debuglog URL.
#

# $sys->reportjsonurl = "https://hooks.slack.com/services/9999";
$sys->reportjsonbase = array("text" => "@body@\n<@link@>");


?>
				if (!preg_match('/^[0-9A-Za-z][-_.@+0-9A-Za-z]*$/', $login))
					log_die("invalid character in login.");