Open
syscontainer-tools代码安全扫描问题 #20
yangna1310Add on  Sep 14
yangna1310
yangna1310Collaborators
Sep 14 Create

【缺陷描述】:请补充详细的缺陷问题现象描述

syscontainer-tools代码安全扫描问题

一、缺陷信息

发现 26 个问题(🔴 致命 2 · 🟡 重要 7 · 🟢 建议 13 · ⚠️ 疑似 4)

其中 2 个 🔴 致命问题(udev 规则注入导致宿主机 root 命令执行、cgroup 路径穿越导致宿主机任意文件写入)可被容器侧/容器配置侧输入触发,构成容器逃逸风险,必须优先修复。


🔴 [致命] pkg/udevd/udevd_controller.go:41-44 — udev 规则注入导致宿主机 root 命令执行

CWE: CWE-78(操作系统命令注入)/ CWE-74(注入)
规则: R02-注入(命令注入 / 模板注入)
描述: Rule.ToUdevRuleString() 将 r.Name(设备 PathOnHost 基名)、r.CtrDevName(设备 PathInContainer)、r.TrimContainerID() 直接以 fmt.Sprintf 拼入 udev 规则字符串的 KERNEL=="..." 与 RUN{program}+="..." 双引号字段,未做任何转义。udev 规则以换行分隔、以 " 界定字符串、以 , 分隔键。若 CtrDevName 或 Name 含 "、, 或 \n,即可闭合 RUN+="..." 字段并注入额外的 udev 键(如新的 RUN+="/bin/sh ...")或新规则行。该规则由 pkg/udevd/udevd.go:38-61 saveRules() 写入 /etc/udev/rules.d/99-syscontainer-tools.rules(root:0600),随后 reloadConfig() 执行 udevadm control --reload 使其生效。当宿主机发生块设备分区 add/remove 事件时,udev 守护进程以 root 身份执行被注入的 RUN 程序。
影响: 容器逃逸 → 宿主机 root 任意命令执行。攻击者只需控制设备路径字符串(含换行或 ",RUN+="),即可在下次分区热插拔时以 root 触发任意命令。
CVSS: 8.5(High,AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
数据来源: 容器设备配置(device_hook.json 中的 PathInContainer/PathOnHost)→ 由 syscontainer-tools add-device CLI 或 hook 流程写入;底层来自 libdevice.ParseMapping(仅校验绝对路径,不校验特殊字符)。
风险传播路径: 容器配置/CLI → ParseMapping(仅 IsAbs+Clean)→ DeviceMapping → hookConfig.Devices → AddDevices → udevdCtrl.AddRule → ToDisk → saveRules 写文件 → udevadm reload → 分区事件 → udev root 执行注入命令。

调用栈

hooks/syscontainer-hooks/prestart.go:74  AddDevices()
  -> hooks/syscontainer-hooks/prestart.go:119  udevdCtrl.AddRule(&udevd.Rule{Name: dev.PathOnHost, CtrDevName: dev.PathInContainer, Container: state.ID})
     -> pkg/udevd/udevd_controller.go:133  (*udevdController).AddRule()
        -> pkg/udevd/udevd_controller.go:165  ToDisk()
           -> pkg/udevd/udevd.go:38  saveRules()
              -> pkg/udevd/udevd.go:51  f.WriteString(r.ToUdevRuleString())   // 注入点
                 -> pkg/udevd/udevd.go:33  reloadConfig()  -> exec("udevadm","control","--reload")

另一入口:libdevice/libdevice.go:147-160(CLI add-device 直接添加磁盘设备规则)。

问题代码

// 文件: pkg/udevd/udevd_controller.go:41-44
func (r *Rule) ToUdevRuleString() string {
	return fmt.Sprintf("KERNEL==\"%s*\",ACTION==\"add|remove\", ENV{DEVTYPE}==\"partition\", SUBSYSTEM==\"block\", RUN{program}+=\"%s $env{ACTION} %s $name %s %s\"",
		filepath.Base(r.Name), programe, r.TrimContainerID(), r.CtrDevName, filepath.Base(r.Name))
}

调用链分析

层次 函数 文件:行号 角色
1 AddDevices hooks/syscontainer-hooks/prestart.go:74 prestart hook 入口,遍历 hookConfig.Devices
2 calcPathForDevice / ParseDevice prestart.go:99 / libdevice/devices_unix.go:117 解析设备,仅校验绝对路径,不过滤 "/,/\n
3 udevdCtrl.AddRule pkg/udevd/udevd_controller.go:133 内存追加规则,未转义
4 ToDisk → saveRules pkg/udevd/udevd.go:38,51 写入 /etc/udev/rules.d/99-syscontainer-tools.rules
5 reloadConfig pkg/udevd/udevd.go:33 udevadm control --reload 使规则生效
6 udev 守护进程 (系统) 分区事件时以 root 执行 RUN → 命令执行

修复建议

// 修复:对拼入 udev 规则的所有用户可控字段做严格白名单校验 + 转义。
// 1) 在 ParseMapping 中限制设备路径字符集(禁止 " , \n \r \\ $ ` 等元字符):
//    var devPathRe = regexp.MustCompile(`^/[A-Za-z0-9_./\-]+$`)
// 2) ToUdevRuleString 中对字段做 udev 安全转义(移除/拒绝 " 与换行):
func (r *Rule) ToUdevRuleString() string {
    name := sanitizeUdevToken(filepath.Base(r.Name))
    ctr  := sanitizeUdevToken(r.CtrDevName)
    cid  := sanitizeUdevToken(r.TrimContainerID())
    return fmt.Sprintf(`KERNEL=="%s*",ACTION=="add|remove",ENV{DEVTYPE}=="partition",SUBSYSTEM=="block",RUN{program}+="%s $env{ACTION} %s $name %s %s"`,
        name, programe, cid, ctr, name)
}
func sanitizeUdevToken(s string) string {
    // 拒绝任何双引号、反斜杠、换行;仅保留安全字符
    if strings.ContainsAny(s, `"\`+"\n\r") {
        return "" // 或上层返回 error 拒绝该规则
    }
    return s
}
// 3) 上层 AddRule 应在 token 非法时返回 error 而非静默写入。

🔴 [致命] libdevice/cgroup.go:58-59 — cgroup 路径穿越导致宿主机任意文件写入/截断

CWE: CWE-22(路径穿越)/ CWE-73(外部控制文件路径)
规则: R02-注入(路径遍历)/ R01-内存与文件边界
描述: FindCgroupPath(pid, subsystem, innerPath) 当 filepath.IsAbs(innerPath) 为真时,直接返回 filepath.Join(cgroupRoot, filepath.Base(mnt), innerPath)。filepath.Join 会清理 .. 段,若 innerPath 为含 .. 的绝对路径(如 /../../../../tmp/evil),结果被清洗为 /tmp/evil,完全逃逸 cgroup 根目录。随后 UpdateCgroupPermission(cgroup.go:81)、UpdateCgroupDeviceReadIOPS/Bps/Weight(cgroup.go:103-172)以 filepath.Join(cgroupPath, "devices.allow"|"blkio.throttle.*") 为目标,调用 ioutil.WriteFile(path, []byte(value), 0600) 写入设备控制字符串。ioutil.WriteFile 使用 O_WRONLY|O_CREATE|O_TRUNC,可在宿主机任意目录创建/截断文件(文件名固定为 devices.allow 等),以 root 身份执行。innerPath 源自 spec.Linux.CgroupsPath(容器 OCI spec,容器创建者可控)。
影响: 容器侧可控输入 → 宿主机 root 任意目录文件创建/截断。结合固定文件名可向 /etc/cron.d/、/etc/sudoers.d/ 等目录投放 devices.allow 文件(cron 会尝试解析 cron.d 下所有文件),或在可预测路径截断关键文件造成 DoS。属宿主机边界突破。
CVSS: 7.5(High,AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H)
数据来源: spec.Linux.CgroupsPath(容器 OCI 配置,由 isulad 依据用户创建请求生成)→ 经 hooks/syscontainer-hooks/prestart.go:78(AddDevices)与 prestart.go:189(UpdateQos)以及 libdevice/libdevice.go:88(add-device CLI 经 c.GetCgroupPath())传入。
风险传播路径: 容器 spec.Linux.CgroupsPath(含 ..)→ FindCgroupPath(IsAbs 分支,Join 清洗后逃逸)→ UpdateCgroup* → ioutil.WriteFile(O_TRUNC) → 宿主机任意目录文件创建/截断(root)。

调用栈

hooks/syscontainer-hooks/prestart.go:74  AddDevices()
  -> hooks/syscontainer-hooks/prestart.go:78  libdevice.FindCgroupPath(pid, "devices", spec.Linux.CgroupsPath)
     -> libdevice/cgroup.go:47  FindCgroupPath()
        -> libdevice/cgroup.go:58  return filepath.Join(cgroupRoot, filepath.Base(mnt), innerPath)   // 穿越点
           -> libdevice/cgroup.go:77  UpdateCgroupPermission(CgroupBase, device, true)
              -> libdevice/cgroup.go:86  ioutil.WriteFile(path, []byte(value), 0600)   // 宿主机任意文件写

CLI 入口:device.go:125 → libdevice.AddDevice → libdevice.go:88-93(c.GetCgroupPath() 返回 isula inspect 的 CgroupParent,亦可能含异常字符)。

问题代码

// 文件: libdevice/cgroup.go:47-74
func FindCgroupPath(pid, subsystem, innerPath string) (string, error) {
	cgroupRoot, err := cgroups.FindCgroupMountpointDir()
	...
	mnt, root, err := cgroups.FindCgroupMountpointAndRoot(subsystem)
	...
	if filepath.IsAbs(innerPath) {                       // 仅判断绝对,未防 ..
		return filepath.Join(cgroupRoot, filepath.Base(mnt), innerPath), nil  // Join 清洗 .. 后逃逸
	}
	...
}
// 文件: libdevice/cgroup.go:77-91
func UpdateCgroupPermission(CgroupBase string, device *types.Device, isAddDevice bool) error {
	...
	path = filepath.Join(CgroupBase, "devices.allow")    // CgroupBase 已被穿越
	if err := ioutil.WriteFile(path, []byte(value), 0600); err != nil {  // O_TRUNC 写入宿主机
		return err
	}
	return nil
}

调用链分析

层次 函数 文件:行号 角色
1 AddDevices / UpdateQos prestart.go:74 / prestart.go:187 hook 入口,取 spec.Linux.CgroupsPath
2 FindCgroupPath libdevice/cgroup.go:47 innerPath 绝对且含 .. 时 Join 后逃逸 cgroupRoot
3 UpdateCgroupPermission / UpdateCgroupDevice* libdevice/cgroup.go:77,94,111,128,145,162 拼接固定文件名并 WriteFile
4 ioutil.WriteFile libdevice/cgroup.go:86 O_WRONLY|O_CREATE|O_TRUNC,root 写宿主机任意目录

修复建议

// 修复:对 innerPath 做严格净化,禁止任何 .. 段并限制在 cgroup 层级内。
func FindCgroupPath(pid, subsystem, innerPath string) (string, error) {
	cgroupRoot, err := cgroups.FindCgroupMountpointDir()
	if err != nil { return "", err }
	mnt, root, err := cgroups.FindCgroupMountpointAndRoot(subsystem)
	if err != nil { return "", err }

	if filepath.IsAbs(innerPath) {
		// 关键修复:先 Clean,再校验不含 .. 且必须位于子系统挂载点之下
		clean := filepath.Clean(innerPath)
		if strings.Contains(clean, "..") {
			return "", fmt.Errorf("invalid cgroups path: %q contains parent reference", innerPath)
		}
		base := filepath.Join(cgroupRoot, filepath.Base(mnt))
		joined := filepath.Join(base, clean)
		// 二次校验结果仍位于 base 之下
		if !strings.HasPrefix(joined+string(filepath.Separator), base+string(filepath.Separator)) && joined != base {
			return "", fmt.Errorf("invalid cgroups path escapes %q", base)
		}
		return joined, nil
	}
	// ... 非 absolute 分支保持原逻辑
}

🟡 [重要] libdevice/container_work.go:112-115 — sysctl key 路径穿越(容器命名空间内任意文件写)

CWE: CWE-22(路径穿越)
规则: R02-注入(路径遍历)/ R01-文件边界
描述: writeSystemProperty(key, value) 将 sysctl key 中的 . 替换为 /,再 path.Join("/proc/sys", keyPath) 后 ioutil.WriteFile(..., 0644)。path.Join 会清洗 ..,若 key 含 ..(如 ../../etc/crontab),目标路径逃逸 /proc/sys,写入容器内任意文件(0644)。
影响: 容器 spec.Linux.Sysctl 的 key 可控 → 容器命名空间内任意文件写入/创建,绕过 sysctl 限制。实际写入发生在容器 mnt 命名空间(reexec 子进程),影响范围限于容器 fs,故降为重要;但破坏了 sysctl 边界,且若未来重构为宿主机侧写入则升级为致命。
CVSS: 4.5(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L)
数据来源: spec.Linux.Sysctl(容器 OCI spec)→ prestart.go:360 AdjustUserns 遍历传入 driver.UpdateSysctl(pid, &types.Sysctl{key, value}) → 经 nsexec reexec 进入容器 mnt ns → doUpdateSysctl → writeSystemProperty。
风险传播路径: 容器 spec.Linux.Sysctl key 含 .. → writeSystemProperty → path.Join 清洗逃逸 /proc/sys → ioutil.WriteFile 0644 容器内任意文件。

调用栈

hooks/syscontainer-hooks/prestart.go:351  AdjustUserns()
  -> hooks/syscontainer-hooks/prestart.go:360  driver.UpdateSysctl(pid, &types.Sysctl{key, value})
     -> libdevice/nsexec/nsexec.go:169  (*nsexecDriver).UpdateSysctl()  // reexec 进入容器 mnt ns
        -> libdevice/container_work.go:99  WorkInContainer() case UpdateSysctlMsg
           -> libdevice/container_work.go:117  doUpdateSysctl(pipe)
              -> libdevice/container_work.go:112  writeSystemProperty(sysctl.Key, sysctl.Value)  // 穿越点
                 -> libdevice/container_work.go:114  ioutil.WriteFile(path.Join("/proc/sys", keyPath), ..., 0644)

问题代码

// 文件: libdevice/container_work.go:110-115
func writeSystemProperty(key, value string) error {
	keyPath := strings.Replace(key, ".", "/", -1)
	return ioutil.WriteFile(path.Join("/proc/sys", keyPath), []byte(value), 0644)  // key 含 .. 则逃逸
}

修复建议

// 修复:校验 key 仅含合法 sysctl 字符(字母/数字/`.`/`_`/`-`),禁止 .. 与绝对路径。
func writeSystemProperty(key, value string) error {
	if !sysctlKeyRe.MatchString(key) { // `^[A-Za-z0-9._\-]+$`
		return fmt.Errorf("invalid sysctl key: %q", key)
	}
	keyPath := strings.Replace(key, ".", "/", -1)
	full := path.Join("/proc/sys", keyPath)
	if !strings.HasPrefix(full, "/proc/sys/") && full != "/proc/sys" {
		return fmt.Errorf("sysctl key %q escapes /proc/sys", key)
	}
	return ioutil.WriteFile(full, []byte(value), 0644)
}

🟡 [重要] main.go:113 / hooks/syscontainer-hooks/main.go:165 — 运行目录以 0666 创建(权限不当)

CWE: CWE-732(关键资源权限不当)
规则: R08-依赖与配置(文件权限)/ R06-隐私
描述: os.MkdirAll(config.IsuladToolsDir, 0666) 以 0666 创建 /run/syscontainer-tools。目录 0666 缺少执行位(无法被遍历进入),且对"组/其他"开放写位。同样代码在 hook main.go:165 重复出现。该目录存放 netns 挂载点文件(/run/syscontainer-tools/netns/<id>,0600)、udevd 锁文件、master 共享路径等敏感资源。
影响: 目录权限设置错误:无执行位导致目录本身不可用(依赖默认 umask 与内核对目录需 x 位的处理产生混乱行为);且若 umask 较松,"其他"用户可写入该目录条目,存在符号链接替换/竞争风险。
CVSS: 3.7(Low)
数据来源: 硬编码 config.IsuladToolsDir = "/run/syscontainer-tools"(config/config.go:33)。

调用栈

main.go:56  mainWork()
  -> main.go:112  app.Before
     -> main.go:113  os.MkdirAll(config.IsuladToolsDir, 0666)   // 权限不当
hooks/syscontainer-hooks/main.go:141  main()
  -> hooks/syscontainer-hooks/main.go:165  os.MkdirAll(hconfig.IsuladToolsDir, 0666)

问题代码

// 文件: main.go:113
if err := os.MkdirAll(config.IsuladToolsDir, 0666); err != nil {
    logrus.Errorf("failed to set syscontainer-tools dir: %v", err)
}

修复建议

// 修复:目录需要执行位,且敏感运行目录应限制为 root 仅可读写执行。
if err := os.MkdirAll(config.IsuladToolsDir, 0750); err != nil {
    return fmt.Errorf("failed to set syscontainer-tools dir: %v", err) // 同时应返回错误而非仅记录
}

🟡 [重要] utils/selinux.go:168-178 — NewContext 索引越界 panic(DoS)

CWE: CWE-754(对异常条件的不当检查)/ CWE-476
规则: R01-内存安全(越界访问)/ R05-错误处理
描述: NewContext(scon) 用 strings.SplitN(scon, ":", 4) 后直接访问 con[0]/con[1]/con[2]/con[3],未校验切片长度 ≥ 4。若 scon(来自 selinux.Getfilecon 读取的 xattr)为格式异常的短 SELinux 标签(字段少于 4 个),触发 index out of range panic,进程崩溃。
影响: relabel 流程崩溃 → 容器 prestart hook 失败 → 容器启动失败(DoS);或 syscontainer-tools relabel CLI 崩溃。SELinux 标签可因文件系统损坏、非 SELinux 文件(xattr 为空/非标准)而异常。
CVSS: 4.0(Medium,AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
数据来源: selinux.Getfilecon(...) 返回的 xattr 字符串 → relabel.go:99(relabel CLI)与 hooks/syscontainer-hooks/relabel.go:205(prestart relabel hook)。

调用栈

relabel.go:64  relabel()
  -> relabel.go:93  selinux.Getfilecon(hostSystemd)  -> attr
     -> relabel.go:99  utils.NewContext(attr)        // panic 点
hooks/syscontainer-hooks/relabel.go:147  preStartNs()
  -> hooks/syscontainer-hooks/relabel.go:201  selinux.Getfilecon(hostRelabelBin)
     -> hooks/syscontainer-hooks/relabel.go:205  utils.NewContext(attr)  // panic 点

问题代码

// 文件: utils/selinux.go:168-178
func NewContext(scon string) SELinuxContext {
	c := make(SELinuxContext)
	if len(scon) != 0 {
		con := strings.SplitN(scon, ":", 4)
		c["user"] = con[0]   // 未校验 len(con)
		c["role"] = con[1]
		c["type"] = con[2]
		c["level"] = con[3]  // len(con)<4 时 panic
	}
	return c
}

修复建议

func NewContext(scon string) SELinuxContext {
	c := make(SELinuxContext)
	if len(scon) != 0 {
		con := strings.SplitN(scon, ":", 4)
		if len(con) < 4 {
			logrus.Warnf("malformed selinux context: %q", scon)
			return c // 或返回 error 由调用方决定
		}
		c["user"], c["role"], c["type"], c["level"] = con[0], con[1], con[2], con[3]
	}
	return c
}

🟡 [重要] libdevice/devices_unix.go:209-210 — FindSubPartition 切片越界 panic(DoS)

CWE: CWE-754 / CWE-125(越界读取)
规则: R01-内存安全(切片边界)
描述: FindSubPartition 将 lsblk 输出按 \n 切分后执行 subDevNames := rawString[1 : len(rawString)-1],未校验 len(rawString) > 1。当 lsblk 输出为空或仅 1 行(设备无子分区、lsblk 异常、设备名无效导致无输出)时,rawString[1 : len-1] 触发 slice bounds out of range panic。对照 types/device.go:154-158 GetBaseDevName 已有 if len(rawString) <= 1 守卫,本函数缺失,属一致性缺陷。
影响: add-device --follow-partition 或 remove-device --follow-partition 在异常 lsblk 输出下崩溃,工具进程退出(DoS),设备管理操作中断。
CVSS: 4.0(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H)
数据来源: lsblk -n -p -r -o NAME <device.PathOnHost> 输出 → 受设备 PathOnHost(CLI,绝对路径)及宿主机块设备状态影响。

调用栈

device.go:72  addDevCommand.Action
  -> device.go:99  getDevices(context)
     -> device.go:354  libdevice.FindSubPartition(device)   // follow-partition 分支
        -> libdevice/devices_unix.go:203  exec.Command("lsblk", ...)
           -> libdevice/devices_unix.go:209  subDevNames := rawString[1 : len(rawString)-1]  // panic

另一入口:libdevice/libdevice.go:289(RemoveDevice follow-partition)→ config.FindSubPartition(不直接调 lsblk,但关联)。

问题代码

// 文件: libdevice/devices_unix.go:200-211
func FindSubPartition(device *types.Device) []*types.Device {
	var subDevices []*types.Device
	cmd := exec.Command("lsblk", "-n", "-p", "-r", "-o", "NAME", device.PathOnHost)
	out, err := cmd.CombinedOutput()
	if err != nil {
		logrus.Errorf("Failed to lsblk %s : %v", string(out), err)
		return subDevices
	}
	rawString := strings.Split(string(out), "\n")
	subDevNames := rawString[1 : len(rawString)-1]   // 缺少 len>1 守卫 → panic
	...
}

修复建议

rawString := strings.Split(string(out), "\n")
// 至少需包含表头行 + 一个数据行 + 末尾空串
if len(rawString) < 3 {
    return subDevices // 无子分区,安全返回
}
subDevNames := rawString[1 : len(rawString)-1]

🟡 [重要] relabel.go:93-94,103,75 — SELinux relabel 错误被吞没,安全标签可能未生效

CWE: CWE-754 / CWE-252(未检查返回值)
规则: R05-错误处理
描述: relabel() 中三处错误处理缺陷:(1) relabel.go:93-94 if attr, err = selinux.Getfilecon(hostSystemd); err != nil { return nil } —— 出错时返回 nil(成功),掩盖失败;(2) relabel.go:103 selinux.Setfilecon(context.path, con.Get()) 返回值被忽略;(3) relabel.go:75 写 file_contexts.local 的错误仅 log 不返回。
影响: SELinux relabel 可能在静默失败下报告"success"(relabel.go:137 打印 "SELinux relabel success"),导致容器在 SELinux 标签未正确设置的情况下运行,安全隔离被削弱而管理员不知情。
CVSS: 4.0(Medium,AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N)
数据来源: selinux.Getfilecon/Setfilecon 对 /lib/systemd/systemd、path/isulad 的 xattr 操作。

调用栈

relabel.go:132  relabelCommand.Action
  -> relabel.go:133  setUpSelinuxLabel(path, rootfs)
     -> relabel.go:110  relabel(path)
        -> relabel.go:93   selinux.Getfilecon(hostSystemd)  // err 时 return nil(伪成功)
        -> relabel.go:103  selinux.Setfilecon(...)          // 返回值忽略
        -> relabel.go:137  fmt.Printf("SELinux relabel success\n")  // 误报成功

问题代码

// 文件: relabel.go:93-104
if attr, err = selinux.Getfilecon(hostSystemd); err != nil {
    return nil   // 错误被吞,返回成功
}
...
for _, context := range modifyContexts {
    con.SetType(context.conType)
    logrus.Infof("%s [%s]", context.path, con.Get())
    selinux.Setfilecon(context.path, con.Get())   // 返回值未检查
}

修复建议

if attr, err = selinux.Getfilecon(hostSystemd); err != nil {
    return fmt.Errorf("getfilecon %s failed: %v", hostSystemd, err) // 返回错误
}
...
if err := selinux.Setfilecon(context.path, con.Get()); err != nil {
    return fmt.Errorf("setfilecon %s failed: %v", context.path, err)
}

🟡 [重要] hooks/syscontainer-hooks/prestart.go:306-342 — 容器可控宿主机内核模块加载

CWE: CWE-269(不当特权管理)/ CWE-863(授权不当)
规则: R02-注入(命令执行)/ R08-依赖与配置
描述: DynLoadModule 从 spec.Process.Env 中解析 KERNEL_MODULES= 环境变量,对每个模块名做 isValidModuleName(正则 ^[A-Za-z0-9\-\_]*$)校验后,在宿主机执行 exec.Command("modprobe", "-a", modules...)。模块名虽经字符白名单校验,但容器(或容器镜像/配置)可指定任意已安装的内核模块名,触发宿主机加载该模块。
影响: 容器侧(拥有设置自身 Env 的权限)可诱导宿主机加载任意已安装内核模块,包括已知存在漏洞的模块,扩大宿主机攻击面、触发宿主机内核模块初始化副作用(资源消耗、设备创建等)。
CVSS: 5.5(Medium,AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L)
数据来源: spec.Process.Env(容器 OCI spec,容器创建者/镜像可控)→ prestart.go:309 遍历。

调用栈

hooks/syscontainer-hooks/main.go:175  prestartHook(hData, ...)
  -> hooks/syscontainer-hooks/prestart.go:469  prestartHook()
     -> hooks/syscontainer-hooks/prestart.go:478  DynLoadModule()  // actions 之一
        -> hooks/syscontainer-hooks/prestart.go:334  exec.Command("modprobe", kernelModules...)  // 宿主机执行

问题代码

// 文件: hooks/syscontainer-hooks/prestart.go:306-342
func DynLoadModule(state *configs.HookState, hookConfig *hconfig.ContainerHookConfig, spec *specs.Spec) error {
	kernelModules := []string{"-a"}
	for _, env := range spec.Process.Env {
		if strings.Contains(env, "KERNEL_MODULES=") {
			...
			envModules := strings.Split(envValue[1], ",")
			for _, module := range envModules {
				if isValidModuleName(module) {
					kernelModules = append(kernelModules, module)  // 容器提供模块名
				}
			}
		}
	}
	...
	cmd := exec.Command("modprobe", kernelModules...)  // 宿主机加载
	err := cmd.Run()
	...
}

修复建议

// 修复:KERNEL_MODULES 不应由容器 spec 控制;改为宿主机管理员配置(如 /etc/syscontainer-tools/modules.conf)。
// 若必须支持容器指定,则维护宿主机允许加载模块白名单,仅允许白名单内模块;并审计每次 modprobe 调用。
var allowedModules = map[string]bool{"vxlan": true, "br_netfilter": true} // 管理员维护
for _, module := range envModules {
    if !allowedModules[module] {
        logrus.Warnf("reject kernel module %q not in allowlist", module)
        continue
    }
    kernelModules = append(kernelModules, module)
}

🟡 [重要] main.go:123 / hooks/syscontainer-hooks/main.go:52 — 日志路径符号链接跟随(root 追加写)

CWE: CWE-59(链接跟随不当)
规则: R06-隐私与数据 / R01-文件边界
描述: os.OpenFile(logpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0600) 以 root 身份打开 --log 指定路径,OpenFile 默认跟随符号链接。若 logpath 是指向敏感文件(如 /etc/crontab、/etc/sudoers)的符号链接,日志内容将以 O_APPEND 追加到该敏感文件。
影响: 拥有设置 --log 的管理员若误指向符号链接,可污染目标文件;或在共享目录中遭预置符号链接攻击(若运行环境存在可被低权限用户写入的目录)。
CVSS: 4.5(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L)
数据来源: CLI --log 标志(main.go:69)与 hook --log 标志(hooks main.go:152)。

调用栈

main.go:112  app.Before
  -> main.go:123  os.OpenFile(logpath, O_CREATE|O_WRONLY|O_APPEND|O_SYNC, 0600)  // 跟随 symlink
     -> main.go:128  logrus.SetOutput(logfile)  // 后续日志追加写入目标

修复建议

// 修复:使用 O_NOFOLLOW 拒绝符号链接,并校验父目录所有权。
fd, err := syscall.Open(logpath, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_APPEND|syscall.O_NOFOLLOW, 0600)
if err != nil { return err }
logfile := os.NewFile(uintptr(fd), logpath)
// 进一步:os.Lstat 校验非 symlink、属主为 root。

🟢 [建议] pkg/udevd/udevd_controller.go:48 — TrimContainerID 越界 panic(防御性)

规则: R01-内存安全 / R05-错误处理
描述: r.Container[:8] 未校验 len(r.Container) >= 8,若容器 ID 短于 8 字符则 panic。isulad 正常输出 64 位十六进制 ID 不受影响,但来自 state.ID(HookState)或异常配置的短 ID 可触发崩溃。
调用栈: udevd_controller.go:139/155(AddRule/RemoveRule 比较)→ udevd_controller.go:48 TrimContainerID。
修复建议: if len(r.Container) < 8 { return r.Container }; return r.Container[:8],并在 AddRule 前校验 Container 非空。


🟢 [建议] utils/selinux.go:138-143 — IsSystemdInit 弱子串匹配

规则: R07-代码质量 / R02-注入(逻辑绕过)
描述: strings.Contains(initPath, "systemd") 仅判断解析后路径是否包含 "systemd" 子串。任何路径名含 "systemd"(如 /opt/systemd-backdoor/sbin/init)即被判为 systemd init,进入 systemd relabel 分支。
调用栈: hooks/syscontainer-hooks/relabel.go:141/274(relabel/prepoststop)→ utils/selinux.go:139。
修复建议: 改为精确匹配:filepath.Base(initPath) == "systemd" 或 initPath == "/lib/systemd/systemd"。


🟢 [建议] utils/selinux.go:61 / 91 — SeconfigGet/Set 解析器脆弱

规则: R07-代码质量 / R05-错误处理
描述: strings.Split(txt, "=") 要求恰好 2 字段,含 = 的值(如 KEY=a=b)被丢弃;未 trim 空白,SELINUX = permissive 形式无法匹配;SeconfigSet 非原子读-改-写(TOCTOU)。
影响: SELinux 配置解析/写入丢值或失效,可能使 SELinux 模式判断错误。
修复建议: 使用 strings.SplitN(txt, "=", 2) + strings.TrimSpace;SeconfigSet 写临时文件后原子 rename。


🟢 [建议] container/container.go:151-153 — GetCgroupPath 字符串切片解析脆弱

规则: R07-代码质量 / R05-错误处理
描述: cgroupPath[1 : len(cgroupPath)-1] 假设 isula inspect 输出为带引号 JSON 字符串,对 null/非引号输出误解析(如 null → ul),得到错误 cgroup 路径。
影响: cgroup 路径错误导致后续设备 cgroup 权限更新失败或写错位置(与 🔴-2 叠加放大风险)。
修复建议: 用 strconv.Unquote 或 json.Unmarshal 解析 isula 输出;显式处理 null/空值。


🟢 [建议] relabel.go:45-51 — relabelIsuladBinary 死代码(含潜在路径拼接风险)

规则: R07-代码质量(死代码)/ R02-注入(潜在)
描述: relabelIsuladBinary(path, bin) 定义后全项目无调用(setUpSelinuxLabel 仅调用 relabel(path))。其内 fmt.Sprintf("%s/%s", path, bin) 将 CLI 输入拼入 semanage fcontext -a 参数,若将来被启用且 path/bin 含特殊字符,存在 SELinux 上下文规则注入/路径穿越隐患。
修复建议: 删除该死代码;如确需启用,对 path/bin 做字符白名单校验并使用 filepath.Join。


🟢 [建议] hooks/syscontainer-hooks/relabel.go:181,275,279 / utils/selinux.go:130,139 — 路径字符串拼接而非 filepath.Join

规则: R02-注入(路径)/ R07-代码质量
描述: 多处以 s.Root + path、state.Root + systemdServiceFile 字符串拼接构造路径,而非 filepath.Join。若 Root 含尾部斜杠或 path 为异常形式,产生 // 或脆弱路径;且对 .. 无防护(依赖上游 Root 可信)。
修复建议: 统一改用 filepath.Join(s.Root, path),并对 Root 做基础校验。


🟢 [建议] utils/utils.go:166-186 — RandomID 弱回退 + RandomFile 死代码

规则: R03-加密与认证(弱随机)/ R07-代码质量
描述: RandomID 在 crypto/rand 失败时回退为 time.Now().UnixNano()(可预测);RandomFile 存在 Stat-后-使用的 TOCTOU 竞争,且经 grep 确认全项目无调用(死代码)。
修复建议: 删除未使用的 RandomFile;RandomID 失败时应返回 error 而非可预测回退。


🟢 [建议] utils/utils.go:82-83 — 默认 syslog 端口错误(541 应为 514)

规则: R08-依赖与配置 / R07-代码质量
描述: syslogDefaultUDPService = "localhost:541"、syslogDefaultTCPService = "localhost:541",标准 syslog 端口为 514,541 非标准,默认回退时 syslog 转发失效。
修复建议: 改为 localhost:514。


🟢 [建议] pkg/ethtool/ethtool.go:64,82,100 — CGo C.CString 与 C 函数边界未验证

规则: R01-内存安全(CGo)/ R08-依赖
描述: 三处 cName := C.CString(etool.name) 后传入 C 函数 setNetDeviceTSO/TX/SG。unsafe.Pointer 仅用于 C.free(本身无害),但若 C 侧 ethtool.h 将设备名拷入固定缓冲(如 strncpy 到 IFNAMSIZ 未截断),存在缓冲溢出隐患。Go 侧未限制 etool.name 长度。
修复建议: Go 侧校验 len(name) < 16(IFNAMSIZ);审查 ethtool.h 确保 strncpy/snprintf 带长度限制。


🟢 [建议] libdevice/libdevice.go:197-200,360-363,446-449,512-515 — 错误聚合丢失上下文

规则: R05-错误处理 / R07-代码质量
描述: 多处将 retErr 切片经 fmt.Sprint 拼成单字符串再 errors.New,丢失错误包装与 errors.Is/As 能力,难以定位具体设备失败。
修复建议: 使用 errors.Join(retErr...)(Go 1.20+)或自定义 multi-error 类型保留链。


🟢 [建议] main.go:165 / hooks/syscontainer-hooks/main.go:150 — 忽略 SIGTERM 无法优雅停止

规则: R07-代码质量 / R05-错误处理
描述: signal.Ignore(SIGINT, SIGHUP, SIGQUIT, SIGTERM) 使工具无法被 init/systemd 优雅终止,强杀可能留下未清理的挂载/锁文件。
修复建议: 仅忽略 SIGINT(前台交互),保留 SIGTERM 处理以执行清理。


🟢 [建议] pkg/udevd/udevd.go:93-117 — loadRules 解析脆弱

规则: R07-代码质量 / R05-错误处理
描述: loadRules 按 , 与 " 分割 udev 规则,以固定下标(cmdArray[2]/cmdArray[4])取容器 ID 与设备名,格式异常时静默 continue,与 🔴-1 注入点同源,且解析/生成不对称。
修复建议: 用结构化 udev 规则模型序列化/反序列化,避免字符串下标硬编码。


🟢 [建议] apply-patch:16-21 — 构建脚本未引用变量(潜在命令注入)

规则: R02-注入(shell)/ R08-依赖与配置
描述: patch -p1 -F1 -s < $line 中 $line 未加引号,若 series.conf 中某行含空格或 $(...)/反引号,将触发单词拆分或命令替换。该脚本为构建期维护者文件,非运行时,风险受限。
修复建议: patch -p1 -F1 -s < "$line" 并校验行内容为合法 patch 文件名。


⚠️ 疑似[重要] libnetwork/bridge/bridge.go:38 / libnetwork/bridge/ovs/driver.go:43,53 — ovs-vsctl 参数注入(bridge 名)

置信度: 55%
描述: exec.Command("ovs-vsctl", "br-exists"|"add-port", bridge, netif) 中 bridge 未校验是否以 - 开头,理论上可被 --version/--option=... 形式注入 ovs-vsctl 选项。netif 因先经 netlink.LinkByName 校验为真实链路名而缓解。
建议: 对 bridge 名做接口名白名单校验(^[A-Za-z0-9._\-]{1,15}$),并在 exec 参数中插入 -- 分隔符:exec.Command("ovs-vsctl", "br-exists", "--", bridge)。


⚠️ 疑似[重要] types/device.go:149,163 / libdevice/devices_unix.go:203 / libdevice/binds_unix.go:117 — lsblk/df 参数注入(设备/路径名)

置信度: 40%
描述: 多处 exec.Command("lsblk"|"df", ..., deviceName|path) 将设备路径作为参数传入,若该值以 - 开头可被解释为工具选项。多数路径经 ParseMapping 校验为绝对路径(以 / 开头)而缓解,但 binds_unix.go:117 的 path(来自 findPathDevice)与 Qos.Path 校验较弱。
建议: 在 exec 调用前统一校验路径以 / 开头,或加入 -- 分隔参数。


⚠️ 疑似[建议] utils/hooks.go:138-144 — GetUIDGid 对 spec.Linux nil 解引用

置信度: 50%
描述: for _, namespace := range spec.Linux.Namespaces 假设 spec.Linux 非 nil;若容器 config.json 缺少 linux 字段,spec.Linux 为 nil 则 panic。isulad 正常生成 Linux 字段而缓解。
建议: 增加 if spec.Linux == nil { return -1, -1 } 守卫。


⚠️ 疑似[建议] libdevice/binds_unix.go:138-139 — findPathDevice df 输出解析脆弱

置信度: 45%
描述: devs := strings.Split(line, " ") 后取 devs[0],df -P 输出含多空格对齐时会产生空串元素,导致 DeviceFromPath("") 误判。非 panic 但解析不可靠。
建议: 使用 strings.Fields(line) 取首字段,并校验非空。


【缺陷所属的os版本】

24.03-LTS-next

【内核版本】

kernel6.6

【缺陷所属软件及版本号】

syscontainer-tools

【环境信息】

【问题复现步骤】

【实际结果】

【期望结果】

【其他相关附件信息】

【缺陷详情及分析指导参考链接】

二、缺陷分析结构反馈
影响性分析说明:

缺陷严重等级:(Critical/High/Moderate/Low)

缺陷根因说明:

受影响版本排查(受影响/不受影响):
openEuler-20.03-LTS-SP4
openEuler-22.03-LTS-SP4
openEuler-24.03-LTS-SP1
openEuler-24.03-LTS-SP3
openEuler-24.03-LTS-SP4

修复是否涉及abi变化(是/否):
openEuler-20.03-LTS-SP4
openEuler-22.03-LTS-SP4
openEuler-24.03-LTS-SP1
openEuler-24.03-LTS-SP3
openEuler-24.03-LTS-SP4

likedislike
openeuler-ci-botopeneuler-ci-botCollaborators
Sep 14 Assign chantsztung as responsible person
openeuler-ci-botopeneuler-ci-botCollaborators
Sep 14 Modify description of issue
openeuler-ci-botopeneuler-ci-botCollaborators
Sep 14 Assign jingxiaolu、Yekelu、haomintsai、yangna1310 as responsible person
openeuler-ci-bot
openeuler-ci-botCollaborators
Sep 14 Comments:

以下的要求不是强制性的, 未按模板评论时对issue无任何影响
issue处理注意事项:
1. 当前issue受影响的分支提交pr时, 须在pr描述中填写当前issue编号进行关联, 否则无法关闭当前issue;
2. 模板内容需要填写完整, 无论是受影响或者不受影响都需要填写完整内容,未引入的分支不需要填写, 否则无法关闭当前issue;
3. 以下为模板中需要填写完整的内容, 请复制到评论区回复, 注: 内容的标题名称(影响性分析说明, 缺陷严重等级, 受影响版本排查(受影响/不受影响), 修复是否涉及abi变化(是/否))不能省略,省略后defect-manager将无法正常解析填写内容.
评论区可能使用到的指令说明:

指令 指令说明 使用权限
/check-issue 触发defect-manager校验 不限
/reason xxx /reason +挂起或取消条件 不限

影响性分析说明:

缺陷严重等级:(Critical/High/Moderate/Low)

缺陷根因说明:

受影响版本排查(受影响/不受影响):

  1. openEuler-20.03-LTS-SP4:
  2. openEuler-22.03-LTS-SP4:
  3. openEuler-24.03-LTS-SP1:
  4. openEuler-24.03-LTS-SP3:
  5. openEuler-24.03-LTS-SP4:

abi变化(是/否):

  1. openEuler-20.03-LTS-SP4:
  2. openEuler-22.03-LTS-SP4:
  3. openEuler-24.03-LTS-SP1:
  4. openEuler-24.03-LTS-SP3:
  5. openEuler-24.03-LTS-SP4:

缺陷issue处理具体操作请参考:
https://atomgit.com/openeuler/cve-manager/blob/master/cve-vulner-manager/doc/md/defect-manager-manual.md
pr关联issue具体操作请参考:
https://docs.atomgit.com/docs/help/home/org_project/pullrequests/pr-related-issue

likedislike
openeuler-ci-botopeneuler-ci-botCollaborators
Sep 14 Added label: sig/iSulad
openeuler-ci-botopeneuler-ci-botCollaborators
Sep 14 Added label: DEFECT/UNFIXED
openeuler-ci-bot
openeuler-ci-botCollaborators
Sep 14 Comments:

Welcome To openEuler Community

Hey @yangna1310 , thanks for your contribution to the community.

Bot Usage Manual

I'm the Bot here serving you. You can find the instructions on how to interact with me at Here . That means you can comment below every pull request or issue to trigger Bot Commands. You can self-configure the PR merge rules for this repository. For more details, please refer to Here.

Contact Guide

If you have any questions, please contact the SIG: iSulad ,
and any of the maintainers: @Yekelu, @chantsztung, @haomintsai, @jingxiaolu ,

likedislike