CVE-2023-37477 (published 2023-07-18T19:15Z)

1Panel is an open source Linux server operation and maintenance management panel. An OS command injection vulnerability exists in 1Panel firewall functionality. A specially-crafted HTTP request can lead to arbitrary command execution. An attacker can make an authenticated HTTP request to trigger this vulnerability. 1Panel firewall functionality `/hosts/firewall/ip` endpoint read user input without validation, the attacker extends the default functionality of the application, which execute system commands. An attacker can execute arbitrary code on the target system, which can lead to a complete compromise of the system. This issue has been addressed in commit `e17b80cff49` which is included in release version `1.4.3`. Users are advised to upgrade. There are no known workarounds for this vulnerability.\n

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-78
cwe_name: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
extended_description: 
		   This weakness can lead to a vulnerability in environments in which the attacker does not have direct access to the operating system, such as in web applications. Alternately, if the weakness occurs in a privileged program, it could allow the attacker to specify commands that normally would not be accessible, or to call alternate commands with privileges that the attacker does not have. The problem is exacerbated if the compromised process does not follow the principle of least privilege, because the attacker-controlled commands may run with special system privileges that increases the amount of damage.
            There are at least two subtypes of OS command injection:
               
				 The application intends to execute a single, fixed program that is under its own control. It intends to use externally-supplied inputs as arguments to that program. For example, the program might use system("nslookup [HOSTNAME]") to run nslookup and allow the user to supply a HOSTNAME, which is used as an argument. Attackers cannot prevent nslookup from executing. However, if the program does not remove command separators from the HOSTNAME argument, attackers could place the separators into the arguments, which allows them to execute their own program after nslookup has finished executing.
				 The application accepts an input that it uses to fully select which program to run, as well as which commands to use. The application simply redirects this entire command to the operating system. For example, the program might use "exec([COMMAND])" to execute the [COMMAND] that was supplied by the user. If the COMMAND is under attacker control, then the attacker can execute arbitrary commands or programs. If the command is being executed using functions like exec() and CreateProcess(), the attacker might not be able to combine multiple commands together in the same line.
               
            From a weakness standpoint, these variants represent distinct programmer errors. In the first variant, the programmer clearly intends that input from untrusted parties will be part of the arguments in the command to be executed. In the second variant, the programmer does not intend for the command to be accessible to any untrusted party, but the programmer probably has not accounted for alternate ways in which malicious attackers can provide input.
         
         

Fix information:
repo_url: https://github.com/1Panel-dev/1Panel
commit_details: repo_url: https://github.com/1Panel-dev/1Panel
author: ssongliu
author_date: 2023-07-17 16:34:29+08:00
msg: fix: 解决部分接口命令注入问题 (#1690)
repository: repo_url: https://github.com/1Panel-dev/1Panel
repo_name: 1Panel-dev/1Panel
description: 🔥🔥🔥 Web-based linux server management control panel. / 现代化、开源的 Linux 服务器运维管理面板。
repo_language: Go
owner: 1Panel-dev
file_changes: filename: container.go
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@ import (
 	"github.com/1Panel-dev/1Panel/backend/buserr"
 	"github.com/1Panel-dev/1Panel/backend/constant"
 	"github.com/1Panel-dev/1Panel/backend/global"
+	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
 	"github.com/1Panel-dev/1Panel/backend/utils/common"
 	"github.com/1Panel-dev/1Panel/backend/utils/docker"
 	"github.com/docker/docker/api/types"
@@ -552,6 +553,9 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
 }
 
 func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error {
+	if cmd.CheckIllegal(container, since, tail) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
 	command := fmt.Sprintf("docker logs %s", container)
 	if tail != "0" {
 		command += " -n " + tail

programming_language: Go
filename: container_compose.go
change_type: ModificationType.MODIFY
diff: @@ -14,8 +14,10 @@ import (
 
 	"github.com/1Panel-dev/1Panel/backend/app/dto"
 	"github.com/1Panel-dev/1Panel/backend/app/model"
+	"github.com/1Panel-dev/1Panel/backend/buserr"
 	"github.com/1Panel-dev/1Panel/backend/constant"
 	"github.com/1Panel-dev/1Panel/backend/global"
+	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
 	"github.com/1Panel-dev/1Panel/backend/utils/compose"
 	"github.com/1Panel-dev/1Panel/backend/utils/docker"
 	"github.com/docker/docker/api/types"
@@ -127,6 +129,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
 }
 
 func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
+	if cmd.CheckIllegal(req.Path) {
+		return false, buserr.New(constant.ErrCmdIllegal)
+	}
 	composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name))
 	if composeItem.ID != 0 {
 		return false, constant.ErrRecordExist
@@ -143,6 +148,9 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
 }
 
 func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) {
+	if cmd.CheckIllegal(req.Name, req.Path) {
+		return "", buserr.New(constant.ErrCmdIllegal)
+	}
 	if err := u.loadPath(&req); err != nil {
 		return "", err
 	}
@@ -177,6 +185,9 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error)
 }
 
 func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
+	if cmd.CheckIllegal(req.Path, req.Operation) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
 	if _, err := os.Stat(req.Path); err != nil {
 		return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
 	}
@@ -195,6 +206,9 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
 }
 
 func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
+	if cmd.CheckIllegal(req.Name, req.Path) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
 	if _, err := os.Stat(req.Path); err != nil {
 		return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
 	}

programming_language: Go
filename: database_mysql.go
change_type: ModificationType.MODIFY
diff: @@ -17,6 +17,7 @@ import (
 	"github.com/1Panel-dev/1Panel/backend/buserr"
 	"github.com/1Panel-dev/1Panel/backend/constant"
 	"github.com/1Panel-dev/1Panel/backend/global"
+	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
 	"github.com/1Panel-dev/1Panel/backend/utils/common"
 	"github.com/1Panel-dev/1Panel/backend/utils/compose"
 	_ "github.com/go-sql-driver/mysql"
@@ -77,6 +78,10 @@ var formatMap = map[string]string{
 }
 
 func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
+	if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Permission) {
+		return nil, buserr.New(constant.ErrCmdIllegal)
+	}
+
 	if req.Username == "root" {
 		return nil, errors.New("Cannot set root as user name")
 	}
@@ -184,6 +189,10 @@ func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error
 }
 
 func (u *MysqlService) ChangePassword(info dto.ChangeDBInfo) error {
+	if cmd.CheckIllegal(info.Value) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
+
 	var (
 		mysql model.DatabaseMysql
 		err   error
@@ -253,6 +262,9 @@ func (u *MysqlService) ChangePassword(info dto.ChangeDBInfo) error {
 }
 
 func (u *MysqlService) ChangeAccess(info dto.ChangeDBInfo) error {
+	if cmd.CheckIllegal(info.Value) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
 	var (
 		mysql model.DatabaseMysql
 		err   error

programming_language: Go
filename: firewall.go
change_type: ModificationType.MODIFY
diff: @@ -304,7 +304,6 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
 		return err
 	}
 	for _, port := range newPorts {
-
 		if err := client.Port(fireClient.FireInfo{Port: strconv.Itoa(port), Protocol: "tcp", Strategy: "accept"}, "add"); err != nil {
 			return err
 		}

programming_language: Go
filename: image_repo.go
change_type: ModificationType.MODIFY
diff: @@ -79,7 +79,7 @@ func (u *ImageRepoService) List() ([]dto.ImageRepoOption, error) {
 
 func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
 	if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) {
-		return buserr.New(constant.ErrRepoConn)
+		return buserr.New(constant.ErrCmdIllegal)
 	}
 	imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(req.Name))
 	if imageRepo.ID != 0 {
@@ -148,7 +148,7 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
 		return errors.New("The default value cannot be deleted !")
 	}
 	if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) {
-		return buserr.New(constant.ErrRepoConn)
+		return buserr.New(constant.ErrCmdIllegal)
 	}
 	repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.ID))
 	if err != nil {

programming_language: Go
filename: ssh.go
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/1Panel-dev/1Panel/backend/app/dto"
+	"github.com/1Panel-dev/1Panel/backend/buserr"
 	"github.com/1Panel-dev/1Panel/backend/constant"
 	"github.com/1Panel-dev/1Panel/backend/global"
 	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -146,6 +147,9 @@ func (u *SSHService) UpdateByFile(value string) error {
 }
 
 func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error {
+	if cmd.CheckIllegal(req.EncryptionMode, req.Password) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
 	currentUser, err := user.Current()
 	if err != nil {
 		return fmt.Errorf("load current user failed, err: %v", err)

programming_language: Go
filename: errs.go
change_type: ModificationType.MODIFY
diff: @@ -42,6 +42,7 @@ var (
 	ErrTypePasswordExpired = "ErrPasswordExpired"
 	ErrNameIsExist         = "ErrNameIsExist"
 	ErrDemoEnvironment     = "ErrDemoEnvironment"
+	ErrCmdIllegal          = "ErrCmdIllegal"
 )
 
 // app
@@ -107,7 +108,6 @@ var (
 	ErrInUsed       = "ErrInUsed"
 	ErrObjectInUsed = "ErrObjectInUsed"
 	ErrPortRules    = "ErrPortRules"
-	ErrRepoConn     = "ErrRepoConn"
 )
 
 // runtime

programming_language: Go
filename: en.yaml
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ ErrNotSupportType: "The system does not support the current type: {{ .detail }}"
 ErrNameIsExist: "Name is already exist"
 ErrDemoEnvironment: "Demo server, prohibit this operation!"
 ErrCmdTimeout: "Command execution timed out！"
+ErrCmdIllegal: "The command contains illegal characters. Please modify and try again!"
 
 #app
 ErrPortInUsed: "{{ .detail }} port already in use"
@@ -83,7 +84,6 @@ ErrTypeOfRedis: "The recovery file type does not match the current persistence m
 #container 
 ErrInUsed: "{{ .detail }} is in use and cannot be deleted"
 ErrObjectInUsed: "This object is in use and cannot be deleted"
-ErrRepoConn: "The repository information contains illegal characters"
 ErrPortRules: "The number of ports does not match, please re-enter!"
 
 #runtime

programming_language: PowerShell
filename: zh-Hant.yaml
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}"
 ErrNameIsExist: "名稱已存在"
 ErrDemoEnvironment: "演示伺服器，禁止此操作!"
 ErrCmdTimeout: "指令執行超時！"
+ErrCmdIllegal: "執行命令中存在不合法字符，請修改後重試！"
 
 #app
 ErrPortInUsed: "{{ .detail }} 端口已被佔用！"
@@ -83,7 +84,6 @@ ErrTypeOfRedis: "恢復文件類型與當前持久化方式不符，請修改後
 #container
 ErrInUsed: "{{ .detail }} 正被使用，無法刪除"
 ErrObjectInUsed: "該對象正被使用，無法刪除"
-ErrRepoConn: "倉庫資訊中存在不合法的字符"
 ErrPortRules: "端口數目不匹配，請重新輸入！"
 
 #runtime

programming_language: Markdown
filename: zh.yaml
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@ ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}"
 ErrNameIsExist: "名称已存在"
 ErrDemoEnvironment: "演示服务器，禁止此操作!"
 ErrCmdTimeout: "命令执行超时！"
+ErrCmdIllegal: "执行命令中存在不合法字符，请修改后重试！"
 
 #app
 ErrPortInUsed: "{{ .detail }} 端口已被占用！"
@@ -83,7 +84,6 @@ ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符，请修改后
 #container 
 ErrInUsed: "{{ .detail }} 正被使用，无法删除"
 ErrObjectInUsed: "该对象正被使用，无法删除"
-ErrRepoConn: "仓库信息中存在不合法的字符"
 ErrPortRules: "端口数目不匹配，请重新输入！"
 
 #runtime

programming_language: Markdown
filename: firewalld.go
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,8 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/1Panel-dev/1Panel/backend/buserr"
+	"github.com/1Panel-dev/1Panel/backend/constant"
 	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
 )
 
@@ -114,6 +116,10 @@ func (f *Firewall) ListAddress() ([]FireInfo, error) {
 }
 
 func (f *Firewall) Port(port FireInfo, operation string) error {
+	if cmd.CheckIllegal(operation, port.Protocol, port.Port) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
+
 	stdout, err := cmd.Execf("firewall-cmd --zone=public --%s-port=%s/%s --permanent", operation, port.Port, port.Protocol)
 	if err != nil {
 		return fmt.Errorf("%s port failed, err: %s", operation, stdout)
@@ -122,6 +128,9 @@ func (f *Firewall) Port(port FireInfo, operation string) error {
 }
 
 func (f *Firewall) RichRules(rule FireInfo, operation string) error {
+	if cmd.CheckIllegal(operation, rule.Address, rule.Protocol, rule.Port, rule.Strategy) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
 	ruleStr := ""
 	if strings.Contains(rule.Address, "-") {
 		std, err := cmd.Execf("firewall-cmd --permanent --new-ipset=%s --type=hash:ip", rule.Address)

programming_language: Go
filename: ufw.go
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,8 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/1Panel-dev/1Panel/backend/buserr"
+	"github.com/1Panel-dev/1Panel/backend/constant"
 	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
 )
 
@@ -131,6 +133,9 @@ func (f *Ufw) Port(port FireInfo, operation string) error {
 	default:
 		return fmt.Errorf("unsupport strategy %s", port.Strategy)
 	}
+	if cmd.CheckIllegal(port.Protocol, port.Port) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
 
 	command := fmt.Sprintf("%s %s %s", f.CmdStr, port.Strategy, port.Port)
 	if operation == "remove" {
@@ -156,6 +161,10 @@ func (f *Ufw) RichRules(rule FireInfo, operation string) error {
 		return fmt.Errorf("unsupport strategy %s", rule.Strategy)
 	}
 
+	if cmd.CheckIllegal(operation, rule.Protocol, rule.Address, rule.Port) {
+		return buserr.New(constant.ErrCmdIllegal)
+	}
+
 	ruleStr := fmt.Sprintf("%s %s ", f.CmdStr, rule.Strategy)
 	if operation == "remove" {
 		ruleStr = fmt.Sprintf("%s delete %s ", f.CmdStr, rule.Strategy)

programming_language: Go