mindxcheckutils/ — 超深度逐行分析

文件行数:345行
核心职责:路径安全校验、文件信息检查、字符串白名单过滤、日志权限管理


一、模块定位

1.1 业务职责

mindxcheckutils 是整个项目的安全基石。所有外部输入(文件路径、命令行参数、环境变量值)都经过此模块校验。它提供:

  1. 路径安全校验 — 防目录穿越(../)、防软链接攻击
  2. 文件安全校验 — 大小限制、权限检查、UID检查、防setuid/setgid
  3. 字符串白名单过滤 — 防注入攻击
  4. 递归文件检查 — 带深度限制的目录递归校验
  5. 日志权限管理 — 运行后收紧日志文件权限
  6. 日志前缀生成 — UID+TTY审计信息

1.2 在系统中的位置

mindxcheckutils 安全层

调用方

runtime/main

runtime/process

hook/main

hook/process

install/process

dcmi/dcManager

CheckPath
路径校验

RealFileChecker
文件校验

RealDirChecker
目录校验

FileChecker
递归检查

CheckFileInfo
文件信息

StringChecker
字符串校验

GetLogPrefix
审计前缀

ChangeRuntimeLogMode
日志权限

1.3 安全威胁模型

防御的威胁

CheckPath

EvalSymlinks

CheckFileInfo

ModeSetuid检查

UID检查

Perm检查

StringChecker

maxDepth

目录穿越
../../../etc/passwd

拒绝含..或./的路径

软链接攻击
/safe/path → /etc/shadow

解析真实路径
不允许时拒绝

超大文件DoS
100GB文件

文件大小限制

权限提升
setuid/setgid

拒绝setuid/setgid文件

非所有者文件
攻击者植入的文件

仅允许root或当前用户

组/其他可写
攻击者可修改

拒绝group/other可写

命令注入
恶意参数

白名单字符过滤

无限递归
环形目录链接

递归深度限制99


二、常量定义

const (
    maxLogFieldLength = 64
    notValidPath      = "not-valid-file-path"
    maxAllowFileSize  = 102400 // in megabytes
    oneMegabytes      = 1048576
    DefaultSize       = 100    // in megabytes
    DefaultWhiteList  = "-_./~"
    DefaultStringSize = 256
    DefaultPathSize   = 4096
    runLogDir                     = api.RunTimeLogDir
    backupLogFileMode os.FileMode = 0400
    runLogFileMode    os.FileMode = 0750
    maxFileNum                    = 32
    getStatErr                    = "failed to get file stat, error: %v"
    groupWriteIndex               = 5
    otherWriteIndex               = 8
    permLength                    = 10
)

var logPrefix = ""
常量 用途
maxAllowFileSize 102400 (MB) 文件大小绝对上限~100GB
DefaultSize 100 (MB) 默认文件大小限制100MB
DefaultWhiteList -_./~ 默认白名单字符
DefaultStringSize 256 字符串最大长度
DefaultPathSize 4096 路径最大长度(与Linux PATH_MAX一致)
backupLogFileMode 0400 日志备份权限(仅所有者可读)
runLogFileMode 0750 日志运行时权限
maxFileNum 32 日志目录最大文件数
groupWriteIndex 5 权限字符串中组写位位置(`-rwx
otherWriteIndex 8 权限字符串中其他写位位置

权限字符串解析

索引:  0  1  2  3  4  5  6  7  8  9
权限:  -  r  w  x  r  w  x  r  w  x
       类型 所有者      组         其他
  • groupWriteIndex=5 — 检查组是否有写权限
  • otherWriteIndex=8 — 检查其他用户是否有写权限

三、核心函数逐行解析

3.1 StringChecker — 字符串白名单校验

func StringChecker(text string, minLength, maxLength int, whiteList string) bool {
    if len(text) <= minLength || len(text) >= maxLength {
        return false
    }
    for _, char := range text {
        if isLetter(char) || isNumber(char) || isInWhiteList(char, whiteList) {
            continue
        }
        return false
    }
    return true
}

func isLetter(c rune) bool {
    return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
}

func isNumber(c rune) bool {
    return '0' <= c && c <= '9'
}

func isInWhiteList(c rune, whiteList string) bool {
    return strings.Contains(whiteList, string(c))
}

逐行解析

  1. 长度检查len(text) <= minLengthlen(text) >= maxLength → 拒绝
    • 注意:使用<=>=,所以minLength=0意味着空字符串也被拒绝
  2. 逐字符遍历
    • 字母(a-z, A-Z) → 允许
    • 数字(0-9) → 允许
    • 在白名单中的字符 → 允许
    • 其他字符 → 拒绝
  3. 所有字符都通过 → 返回true

白名单字符说明

字符 用途 在路径中的含义
- 连字符 文件名中常见
_ 下划线 文件名中常见
. 当前目录/文件扩展名
/ 斜杠 路径分隔符
~ 波浪号 Home目录
空格 调用方追加(参数分隔)

安全分析

  • ✅ 防止shell注入:;, |, &, $, `, () 都不在白名单中
  • ✅ 防止路径穿越:.. 中的.在白名单中,但CheckPath会额外检查
  • ⚠️ 注意:StringChecker仅检查字符级别,不检查语义级别(如../的字符都合法)

不满足

满足

字母

数字

在白名单中

其他

StringChecker

长度检查
minLength < len < maxLength

返回false

逐字符遍历

当前字符

允许,继续

还有更多字符?

返回true

3.2 CheckPath — 路径安全校验

func CheckPath(path string, allowLink bool) error {
    if !StringChecker(path, 0, DefaultPathSize, DefaultWhiteList) {
        return fmt.Errorf("invalid path")
    }

    if strings.Contains(path, "..") || strings.Contains(path, "./") {
        return fmt.Errorf("err path %s", path)
    }
    filePath, err := filepath.Abs(path)
    if err != nil {
        return fmt.Errorf("get abs path failed %v", err)
    }
    if len(filepath.Base(filePath)) > DefaultStringSize {
        return fmt.Errorf("path too long: %s", path)
    }
    realPath, err := filepath.EvalSymlinks(filePath)
    if err != nil || (realPath != filePath && !allowLink) {
        return fmt.Errorf("symlinks or not existed, failed %v, %v, %v, %v", filePath, err, realPath, allowLink)
    }
    return nil
}

逐行解析

  1. 字符级校验:StringChecker检查字符白名单
  2. 目录穿越防御:拒绝包含.../的路径
  3. 绝对路径转换filepath.Abs将相对路径转为绝对路径
  4. 文件名长度限制:Base名不超过256字符
  5. 软链接检查
    • EvalSymlinks 解析所有符号链接获取真实路径
    • 如果真实路径与原始路径不同且!allowLink → 拒绝
    • 如果解析失败(文件不存在) → 拒绝

安全层次

层次 检查 防御目标
字符级 StringChecker 非法字符注入
语义级 .../ 检查 目录穿越
长度级 Base名≤256 缓冲区溢出
链接级 EvalSymlinks 软链接攻击

3.3 CheckFileInfo — 文件信息安全校验

func CheckFileInfo(fileInfo *os.File, size int) error {
    if fileInfo == nil {
        return fmt.Errorf("file info is nil")
    }
    fileStat, err := fileInfo.Stat()
    if err != nil {
        return fmt.Errorf("invalid file")
    }

    if !fileStat.Mode().IsRegular() {
        return fmt.Errorf("invalid regular file")
    }
    if size > maxAllowFileSize || size < 0 {
        return fmt.Errorf("invalid size")
    }
    if fileStat.Size() > int64(size)*int64(oneMegabytes) {
        return fmt.Errorf("size too large")
    }
    perm := fileStat.Mode().Perm().String()
    if len(perm) != permLength {
        return fmt.Errorf("permission not right %v %v", fileInfo.Name(), perm)
    }
    for index, char := range perm {
        switch index {
        case groupWriteIndex, otherWriteIndex:
            if char == 'w' {
                return fmt.Errorf("write permission not right %v %v", fileInfo.Name(), perm)
            }
        default:
        }
    }
    stat, ok := fileStat.Sys().(*syscall.Stat_t)
    if !ok {
        return fmt.Errorf("can not get stat %v", fileInfo.Name())
    }
    uid := int(stat.Uid)
    if !(uid == 0 || uid == os.Getuid()) {
        return fmt.Errorf("owner not right %v %v", fileInfo.Name(), uid)
    }

    if fileStat.Mode()&os.ModeSetuid != 0 {
        return fmt.Errorf("setuid not allowed %v", fileInfo.Name())
    }
    if fileStat.Mode()&os.ModeSetgid != 0 {
        return fmt.Errorf("setgid not allowed %v", fileInfo.Name())
    }
    return nil
}

逐行解析

  1. nil检查:防止空指针
  2. 获取文件信息fileInfo.Stat()
  3. 常规文件检查!fileStat.Mode().IsRegular() — 拒绝设备文件、管道、套接字等
  4. 大小参数校验:size参数本身有效性检查
  5. 文件大小检查fileStat.Size() > size * 1MB — 防止超大文件DoS
  6. 权限字符串检查
    • Perm().String() 返回如 "-rwxr-xr-x" (10字符)
    • 长度必须为10
    • 索引5(组写位)不能为’w’
    • 索引8(其他写位)不能为’w’
  7. UID检查stat.Uid — 文件所有者必须是root(0)或当前用户
  8. setuid检查ModeSetuid — 拒绝setuid文件(防止权限提升)
  9. setgid检查ModeSetgid — 拒绝setgid文件

权限检查详解

-rwx|rwx|rwx
     ↑   ↑
     索引5 索引8
     组写  其他写
     不能w 不能w

设计意图:确保配置文件、可执行文件等关键文件不被组用户或其他用户篡改。只有文件所有者(root或当前用户)可以修改。

CheckFileInfo

nil检查

Stat获取文件信息

是常规文件?

invalid regular file

size参数有效?

invalid size

文件大小 ≤ size*1MB?

size too large

权限字符串长度=10?

permission not right

组写位='w'?

write permission not right

其他写位='w'?

write permission not right

UID=0或当前用户?

owner not right

有setuid?

setuid not allowed

有setgid?

setgid not allowed

返回nil

3.4 RealFileChecker — 完整文件安全校验

func RealFileChecker(path string, checkParent, allowLink bool, size int) (string, error) {
    if !StringChecker(path, 0, DefaultPathSize, DefaultWhiteList) {
        return notValidPath, fmt.Errorf("invalid path")
    }
    _, err := FileChecker(path, false, checkParent, allowLink, 0)
    if err != nil {
        hwlog.RunLog.Errorf("failed to check files, error: %v", err)
        return notValidPath, err
    }
    realPath, err := filepath.Abs(path)
    if err != nil {
        return notValidPath, err
    }
    realPath, err = filepath.EvalSymlinks(realPath)
    if err != nil {
        return notValidPath, err
    }
    fileInfo, err := os.Stat(realPath)
    if err != nil {
        return notValidPath, err
    }
    if !fileInfo.Mode().IsRegular() {
        return notValidPath, fmt.Errorf("invalid regular file")
    }
    if size > maxAllowFileSize || size < 0 {
        return notValidPath, fmt.Errorf("invalid size")
    }
    if fileInfo.Size() > int64(size)*int64(oneMegabytes) {
        return notValidPath, fmt.Errorf("size too large")
    }
    return realPath, nil
}

与CheckFileInfo的区别

维度 RealFileChecker CheckFileInfo
输入 路径字符串 *os.File句柄
返回 真实路径字符串 无返回值
路径校验 StringChecker + FileChecker
软链接 EvalSymlinks解析
权限检查 FileChecker中完成 直接检查
使用场景 从路径校验文件 从已打开文件校验

执行流程

  1. StringChecker字符校验
  2. FileChecker递归校验(包括权限、UID、setuid/setgid检查)
  3. 获取绝对路径
  4. 解析符号链接
  5. Stat获取文件信息
  6. 常规文件检查
  7. 大小检查
  8. 返回真实路径

3.5 RealDirChecker — 目录安全校验

func RealDirChecker(path string, checkParent, allowLink bool) (string, error) {
    if !StringChecker(path, 0, DefaultPathSize, DefaultWhiteList) {
        return notValidPath, fmt.Errorf("invalid path")
    }
    _, err := FileChecker(path, true, checkParent, allowLink, 0)
    if err != nil {
        return notValidPath, err
    }
    realPath, err := filepath.Abs(path)
    if err != nil {
        return notValidPath, err
    }
    realPath, err = filepath.EvalSymlinks(realPath)
    if err != nil {
        return notValidPath, err
    }
    fileInfo, err := os.Stat(realPath)
    if err != nil {
        return notValidPath, err
    }
    if !fileInfo.IsDir() {
        return notValidPath, fmt.Errorf("not a dir")
    }
    return realPath, nil
}

与RealFileChecker的区别

  • FileChecker(path, true, ...) — 第二参数allowDir=true
  • 最后检查 fileInfo.IsDir() 而非 IsRegular()
  • 无大小检查(目录没有大小限制)

3.6 FileChecker — 递归文件/目录校验

func FileChecker(path string, allowDir, checkParent, allowLink bool, deep int) (bool, error) {
    const maxDepth, groupWriteIndex, otherWriteIndex, permLength int = 99, 5, 8, 10
    if deep > maxDepth {
        return false, fmt.Errorf("over maxDepth %v", maxDepth)
    }
    if strings.Contains(path, "..") || strings.Contains(path, "./") {
        return false, fmt.Errorf("err path %s", path)
    }
    filePath, err := filepath.Abs(path)
    if err != nil {
        return false, fmt.Errorf("get abs path failed %v", err)
    }
    if len(filepath.Base(filePath)) > DefaultStringSize {
        return false, fmt.Errorf("path too long")
    }
    fileInfo, ok, err := normalFileCheck(filePath, allowDir, allowLink)
    if err != nil {
        return ok, err
    }
    perm := fileInfo.Mode().Perm().String()
    if len(perm) != permLength {
        return false, fmt.Errorf("permission not right %v %v", filePath, perm)
    }
    for index, char := range perm {
        switch index {
        case groupWriteIndex, otherWriteIndex:
            if char == 'w' {
                return false, fmt.Errorf("write permission not right %v %v", filePath, perm)
            }
        default:
        }
    }
    stat, ok := fileInfo.Sys().(*syscall.Stat_t)
    if !ok {
        return false, fmt.Errorf("can not get stat %v", filePath)
    }
    uid := int(stat.Uid)
    if !(uid == 0 || uid == os.Getuid()) {
        return false, fmt.Errorf("owner not right %v %v", filePath, uid)
    }
    if filePath != "/" && checkParent {
        return FileChecker(filepath.Dir(filePath), true, true, allowLink, deep+1)
    }
    return true, nil
}

逐行解析

  1. 递归深度检查deep > 99 → 拒绝(防无限递归)
  2. 目录穿越检查.../
  3. 绝对路径转换
  4. 文件名长度限制:Base名≤256
  5. normalFileCheck:检查文件类型、软链接、setuid/setgid
  6. 权限检查:组写位和其他写位不能为’w’
  7. UID检查:文件所有者必须是root或当前用户
  8. 递归检查父目录:如果checkParent=true且不是根目录,递归向上检查父目录

递归检查设计意图:即使目标文件本身安全,如果其父目录可被其他用户写入,攻击者可以替换文件。因此需要递归向上检查整个路径的所有组件。

失败

成功

FileChecker
path + deep

deep > 99?

over maxDepth

含..或./?

err path

filepath.Abs
转绝对路径

Base名 ≤ 256?

path too long

normalFileCheck
类型+链接+setuid

返回错误

权限字符串检查

组/其他写位='w'?

write permission

UID=0或当前用户?

owner not right

checkParent且非根?

递归检查父目录
FileChecker Dir, deep+1

返回true, nil

3.7 normalFileCheck — 基础文件检查

func normalFileCheck(filePath string, allowDir bool, allowLink bool) (os.FileInfo, bool, error) {
    realPath, err := filepath.EvalSymlinks(filePath)
    if err != nil || (realPath != filePath && !allowLink) {
        return nil, false, fmt.Errorf("symlinks or not existed, failed %v, %v", filePath, err)
    }
    fileInfo, err := os.Stat(filePath)
    if err != nil {
        return nil, false, fmt.Errorf("get file stat failed %v", err)
    }
    if allowDir {
        if !fileInfo.Mode().IsRegular() && !fileInfo.IsDir() {
            return nil, false, fmt.Errorf("not regular file/dir %v", filePath)
        }
    } else {
        if !fileInfo.Mode().IsRegular() {
            return nil, false, fmt.Errorf("not regular file %v", filePath)
        }
    }
    if fileInfo.Mode()&os.ModeSetuid != 0 {
        return nil, false, fmt.Errorf("setuid not allowed %v", filePath)
    }
    if fileInfo.Mode()&os.ModeSetgid != 0 {
        return nil, false, fmt.Errorf("setgid not allowed %v", filePath)
    }
    return fileInfo, false, nil
}

逐行解析

  1. 软链接解析EvalSymlinks 获取真实路径
    • 如果解析失败 → 拒绝(文件不存在或循环链接)
    • 如果真实路径≠原始路径且!allowLink → 拒绝
  2. Stat获取文件信息
  3. 文件类型检查
    • allowDir=true:允许文件或目录
    • allowDir=false:仅允许常规文件
  4. setuid检查ModeSetuid → 拒绝
  5. setgid检查ModeSetgid → 拒绝

3.8 GetLogPrefix — 审计前缀生成

func GetLogPrefix() (string, error) {
    if logPrefix != "" {
        return logPrefix, nil
    }
    uid := os.Geteuid()
    if uid < 0 {
        return "", fmt.Errorf("err: uid not right")
    }
    tty, err := filepath.EvalSymlinks("/proc/self/fd/0")
    if err != nil || !StringChecker(tty, 0, DefaultStringSize, "/:") {
        tty = "unknown"
    }

    logPrefix = fmt.Sprintf("uid: %d tty: %v ", uid, tty)
    return logPrefix, nil
}

逐行解析

  1. 缓存机制logPrefix全局变量,首次调用后缓存结果
  2. 获取有效用户IDos.Geteuid() — 获取当前进程的有效UID
  3. 获取终端路径/proc/self/fd/0 是stdin的符号链接,指向实际终端
    • EvalSymlinks 解析链接
    • 如果失败或包含非法字符 → 设为"unknown"
    • 注意:白名单中包含/:``,允许/dev/pts/0`这样的路径
  4. 格式化前缀"uid: 0 tty: /dev/pts/0 "

设计意图:审计日志需要知道是谁在什么终端执行了操作。UID和TTY是基本的审计信息。

3.9 ChangeRuntimeLogMode — 日志权限管理

func ChangeRuntimeLogMode(runLog string) error {
    runLogDirLen := len(runLogDir)
    counter := 0
    err := filepath.Walk(runLogDir, func(fileOrPath string, fileInfo os.FileInfo, err error) error {
        counter += 1
        if counter > maxFileNum {
            return fmt.Errorf("the counter file is over maxFileNum")
        }
        if err != nil {
            hwlog.RunLog.Errorf("prevent panic by handling failure accessing a path %q: %v", fileOrPath, err)
            return err
        }
        hasLogPrefix := strings.HasPrefix(fileOrPath[runLogDirLen:], runLog)
        if !hasLogPrefix {
            return nil
        }
        if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
            return fmt.Errorf("the file or path is symlink")
        }
        if errChmod := os.Chmod(fileOrPath, backupLogFileMode); errChmod != nil {
            hwlog.RunLog.Errorf("set file mode %s failed, error: %v", fileOrPath, err)
            return fmt.Errorf("set file mode %s failed", fileOrPath)
        }
        return nil
    })
    if err != nil {
        hwlog.RunLog.Warnf("change runtime log mode failed, error: %v", err)
        return nil
    }
    return nil
}

逐行解析

  1. runLogDirLen — 日志目录路径长度,用于后续前缀匹配
  2. counter — 文件计数器,超过32个文件则终止(安全限制)
  3. filepath.Walk — 递归遍历日志目录
  4. 文件计数counter > maxFileNum(32) → 返回错误(防DoS)
  5. 前缀匹配fileOrPath[runLogDirLen:] 获取相对路径,检查是否以runLog参数开头
    • 例如:runLog="runtime-run-",匹配 runtime-run-20260726.log
  6. 软链接检查:如果是符号链接 → 拒绝(防符号链接攻击)
  7. 修改权限os.Chmod(fileOrPath, 0400) — 仅所有者可读
  8. 错误容忍:Walk返回错误时仅记录Warn日志,不返回错误(确保不影响主流程)

ChangeRuntimeLogMode
runLog=runtime-run-

遍历日志目录

文件数 > 32?

返回错误: over maxFileNum

文件名以runLog开头?

跳过此文件

是符号链接?

返回错误: is symlink

Chmod 0400
仅所有者可读

还有更多文件?

返回nil


四、安全检查矩阵

检查项

输入类型 → 检查函数

环境变量值

StringChecker

命令行参数

StringChecker

文件路径

CheckPath

RealFileChecker

目录路径

RealDirChecker

已打开文件

CheckFileInfo

字符白名单

长度限制

目录穿越防御

软链接检查

常规文件检查

文件大小限制

权限检查
组/其他不可写

UID检查
仅root或当前用户

setuid/setgid检查

递归父目录检查

检查项 防御目标 实现函数
字符白名单 命令注入 StringChecker
长度限制 缓冲区溢出/DoS StringChecker
目录穿越 访问越权文件 CheckPath/FileChecker
软链接 链接攻击 EvalSymlinks
常规文件 设备文件/管道攻击 IsRegular()
文件大小 DoS CheckFileInfo
组/其他不可写 篡改攻击 Perm()检查
UID检查 非授权文件 Stat_t.Uid
setuid/setgid 权限提升 ModeSetuid/ModeSetgid
递归深度 无限递归DoS maxDepth=99
递归父目录 父目录替换攻击 FileChecker递归

五、函数调用关系图

内部函数

公开函数

CheckPath

CheckFileInfo

RealFileChecker

RealDirChecker

FileChecker

StringChecker

GetLogPrefix

ChangeRuntimeLogMode

normalFileCheck

isLetter

isNumber

isInWhiteList

EvalSymlinks

fileInfo.Stat

Perm检查

UID检查

setuid检查

setgid检查

EvalSymlinks

os.Stat

大小检查

EvalSymlinks

os.Stat

IsDir检查

Perm检查

UID检查

setuid检查

递归FileChecker

EvalSymlinks

os.Stat

setuid检查

setgid检查

os.Geteuid

EvalSymlinks
/proc/self/fd/0

StringChecker

filepath.Walk

os.Chmod 0400


六、关键设计决策

决策 原因 实现
字符级白名单 防注入比黑名单更安全 StringChecker仅允许字母+数字+白名单字符
组/其他不可写 防止非授权用户修改关键文件 Perm检查索引5和8不能为’w’
仅root或当前用户所有 防止攻击者植入文件 Stat_t.Uid检查
拒绝setuid/setgid 防止权限提升 ModeSetuid/ModeSetgid检查
递归检查父目录 防止父目录可写导致文件被替换 FileChecker递归向上
递归深度限制99 防无限递归DoS deep > maxDepth
文件计数限制32 防遍历DoS ChangeRuntimeLogMode中counter
日志权限收紧0400 日志含敏感信息 ChangeRuntimeLogMode Chmod
缓存日志前缀 避免重复读取/proc logPrefix全局变量
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐