openUBMC社区安全编码规范
更新时间: 2026/05/14
在Gitcode上查看源码

openUBMC社区安全编码规范

概念解释

提权】:安全常用术语,一个用户能通过某种非预期的方式,突破该用户的权限设计,完成正常情况下无法完成的某些操作,就称为提权

外部输入】:来自进程之外的数据就称为外部输入,包括:

  • 文件(包括data分区的程序配置文件);
  • 注册表;
  • 网络报文和北向接口;
  • 环境变量;
  • 命令行输入;
  • 用户态数据(对于内核程序而言);
  • 进程间通信(包括管道、消息队列、socket、RPC等);
  • 函数参数(对于全局API而言);
  • 来自硬件的数据(如芯片寄存器,PMBus/SMBus/MCTP接收的数据等)。

外部可控】:安全常用术语,是指能够被外部用户控制其数据内容的外部输入。外部可控分成以下两种,我们在编码时都需要考虑:

  • 直接外部可控:数据本身是直接来自外部用户的某种输入,例如来自接口调用、网络报文等;
  • 间接外部可控:外部用户无法直接控制数据内容,但是通过利用其他安全漏洞之后,就能够间接修改数据内容,这种称为间接外部可控。例如/etc/passwd文件内容不是外部用户可以直接输入的,但是当软件存在任意文件写漏洞时,攻击者就可以控制/etc/passwd文件的内容,因此间接外部可控也存在潜在的风险。

【skynet】:一种轻量级开源服务端框架,当前openUBMC主要基于skynet框架进行组件app开发。

1 公共原则

P.SEC.B01 禁止组件自行封装私有安全检查函数

【规则说明】

安全检查函数,如检查文件路径、检查shell命令注入字符等,具有一定的专业性,组件自己封装,容易出现场景遗漏等问题,也不利于长期维护。因此各组件应该优先使用框架提供的公共安全检查函数(详见G.SEC.FIL.B02),如果识别到框架能力不满足,则应向社区反馈需求。

2 注入缺陷

注入缺陷包括很多种类型,包括命令注入、代码注入、SQL注入、日志注入等,对系统安全的威胁非常大,可能造成任意指令执行、提权、信息泄露等。其漏洞原理是很多文本型语言的控制字符和数据字符混合在一起,并且控制字符成对出现,攻击者如果能够输入任意字符,他就能够输入控制字符闭合开发者的控制字符,再插入攻击者的语法注入恶意语句。

G.SEC.INJ.B01 禁止使用os.execute、io.popen函数

【规则说明】

使用os.execute、io.popen函数执行shell命令会出现概率性进程卡死的问题,因此openUBMC禁止使用这两个函数。在需要执行某个shell命令的场景,优先看是否可以直接使用C库函数(例如调用C库chmod函数来替代Linux chmod命令)来实现,如果不能,应该使用vos.system_s函数来执行shell命令。

【案例一】

lua
-- 错误示例,使用os.execute可能导致进程卡死
os.execute('cp /tmp/config.json /dev/shm/config.json')
-- 正确示例,使用vos.system_s执行shell命令
vos.system_s('/bin/cp', '/tmp/config.json', '/dev/shm/config.json')

G.SEC.INJ.B02 以/bin/sh或者/bin/bash作为命令解释器时,需要对vos.system_s函数进行命令注入参数检查

【规则说明】

vos.system_s是openUBMC封装的供Lua程序调用的C函数库。如果该函数指定的命令解释器(即函数的第一个参数)是/bin/sh或者/bin/bash则存在命令注入风险,则需要对命令参数进行命令注入检查,框架已经提供公共检查函数check_shell_special_character_s

【例外项】

如果vos.system_s函数的参数均为硬编码,不包括可能是外部输入的内容,则可以不校验。

【案例一】

lua
-- 错误示例:
vos.system_s('/bin/sh', '-c', 'cp', src_dir, '/tmp/test') -- 假设src_dir是外部输入

-- 正确示例: 不使用/bin/sh作为命令解释器
vos.system_s('/bin/cp', src_dir, 'dest_dir') -- 假设src_dir是外部输入

-- 正确示例:使用/bin/sh作为命令解释器,但是有对参数进行校验
local utils_file = require 'utils.file'
if utils_file.check_shell_special_character_s(src_dir) ~= 0 then
   return
end
vos.system_s('/bin/sh', '-c', 'cp', src_dir, '/tmp/test') -- 假设src_dir是外部输入

G.SEC.INJ.B03 执行shell命令时,查找、匹配等操作条件要精准

【规则说明】

执行shell命令时,对文件、进程、用户等的查找,匹配等操作要按照实际查找对象的类型进行查找,防止存在进程仿冒、用户仿冒等情况出现。

c
// 错误示例:原始代码, 在满足进程信息包含sshd的情况下,可以构造特殊输入(例如传入'00'),从而杀掉其他sshd进程
snprintf_s(cmd_str, MAX_CMD_LENGTH, MAX_CMD_LENGTH - 1,
    "ps -ef | grep -w \"%s\" | grep -w sshd | grep -vw \"/usr/sbin/sshd\" | \
     cut -c 10-15 | xargs kill -9 > /dev/null 2>&1", escaped_user);

// 正确示例:按用户名查找
snprintf_s(cmd_str, MAX_CMD_LENGTH, MAX_CMD_LENGTH - 1,
    "ps -ef | grep -w sshd |awk '{print $2,$9}'|grep -w \'%s\' |awk '{print $1}'| \
    xargs kill -9 > /dev/null 2>&1", escaped_user);

G.SEC.INJ.B04 外部输入作为require、load、loadfile、dofile、loadstring等函数参数时,需要进行白名单校验

【规则说明】

requireloadloadfiledofileloadstring等函数可以执行指定路径代码文件或指定字符串的代码块,存在代码注入风险,严禁将可能包含外部输入的数据直接作为这些函数的参数。如果必须使用,应该对参数进行白名单校验,确保执行内容安全可控。以下是Lua语言中存在代码注入风险的危险函数介绍。

lua
require
- 原型:require([modulename])
- 解释:用于加载一个模块代码,如果代码文件是一个代码块,则会执行该代码库。require会搜索目录加载文件,并且会判断是否文件已经加载避免重复加载同一文件 。

load
- 原型:load (chunk [, chunkname [, mode [, env]]])
- 解释:load实现Lua 的反射机制,加载一个代码块,如果chunk是一个字符串,代码块就是这个字符串,chunk也可以是一个函数,通过函数调用获取代码块。如果chunk是一个字符串,且外部可控的话则存在安全风险,攻击者可以通过控制chunk导致一个代码注入。

loadfile
- 原型:loadfile ([filename [, mode [, env]]])
- 解释:通load相似,只是代码块是从文件中获取,如果文件内容可以外部控制,仍然存在代码注入风险。

dofile
- 原型:dofile([filename])
- 解释:加载文件中的代码,如果filename可控,则可以通过新增一个Lua脚本,将filename指定到新增脚本实现代码注入,如果文件内容可控则可以直接修改文件内容,导致代码注入问题。

loadstring
- 原型:loadstring(string [,chunkname])
- 解释:函数会从所给的字符串中来加载程序块并运行,如果省略参数`chunkname`,那么它默认为所给的字符串。

G.SEC.INJ.B05 执行自定义SQL语句需要进行预编译

【规则说明】

某些复杂数据库操作,无法通过libmc封装的简单接口,如selectupdateinsert来完成,需要使exec函数执行自定义的SQL语句。如果这些自定义的SQL拼接了外部输入,则存在SQL注入的风险,预编译可以有效消除此风险。

【例外项】

如果自定义SQL语句是硬编码,不存在外部输入的内容,则可以不进行预编译。

lua
-- 错误示例:自定义SQL语句未进行预编译,存在SQL注入风险
local cmd = 'select * from tb_test where level = ' .. level -- 假设level是外部输入
db:exec(cmd)

-- 正确示例
local cmd = 'select * from tb_test where level = ?'
local vm = db:prepare(cmd) -- 通过框架封装的prepare函数执行预编译
vm:bind_values(level)
vm:step()
vm:finalize()

G.SEC.INJ.B06 在通过字符串拼装Lua脚本进行执行的场景,需要在check_shell_special_character_s的检查后根据脚本场景额外校验特殊字符

【规则说明】

在如worker机制中通过字符串拼装Lua脚本进行执行的场景,还需根据该脚本场景再排除包含可能提前闭环内层函数并注入命令的字符,如“'”、“)”、“ ”(Lua语法中使用空格即可作为命令分隔符,无需使用换行或“;”、“)”)

G.SEC.INJ.B07 skynet.sleep函数入参必须是整数

【规则说明】

skynet.sleep函数入参必须是整数,如果传入浮点数,会抛出异常

lua
-- 假设变量ms的单位是毫秒,则:
-- 正确示例:
skynet.sleep(ms // 10)
-- 错误示例:
skynet.sleep(ms / 10)

G.SEC.INJ.B08 使用框架提供的worker模块做线程操作时,禁止使用string.format方式在代码中拼接参数

【规则说明】

使用框架提供的worker模块(local worker = require ‘worker.core’)做线程操作时,禁止使用string.format方式在代码中拼接参数

lua
【反例】
-- 正常new_path参数:/var/log/tmp.log。
-- 异常new_path参数:/var/log/tmp.log’);print('hello    异常场景下,会造成任意lua代码攻击
work:start(string.format([[
    local file_sec = require 'utils.file'
    file_sec.copy_file_s('%s', '%s')
]], tmp_path, new_path))

【正例】
work:start([[
    local file_sec = require 'utils.file'
    local tmp_path = worker:recv()
    local new_path = worker:recv()
    file_sec.copy_file_s(tmp_path, new_path)
]])

work:send(tmp_path, true)
work:send(new_path, true)

3 文件操作

Lua提供的文件操作函数,和C语言一样,也存在通过软链接或../进行路径跨域。文件操作时未对不可信的路径进行标准化和校验或者校验不充分,导致路径遍历问题,被攻击者越权读写文件。

G.SEC.FIL.B01 创建文件或目录之后,需显式修改文件或目录的权限和属主

【规则说明】

遵循最小权限原则,及时修改文件权限和属主,避免因文件权限过大被低权限用户修改而存在安全风险。

G.SEC.FIL.B02 对于路径外部可控的文件进行任何操作前必须校验路径是否合法

【规则说明】

对于外部可控的文件路径,攻击者可以指定一个软链接路径或者通过../来实现路径跨越,从而造成任意文件的访问。为了防范这类风险,在文件的创建、删除、读写、复制、移动、修改权限或属主等操作之前,必须先进行路径标准化并校验路径是否在预期的目录下,否则容易造成提权、任意文件读写、任意文件删除、信息泄露等安全风险。

针对不同场景的文件路径校验需求,openUBMC封装了C语言和Lua语言版本的文件安全操作库。

c
// C语言版本
// 文件安全打开,返回文件描述符,打开文件前会校验文件是否是绝对化路径,及是否存在软链接,是否在指定路径
gint32 open_s(const gchar *pathname, guint32 flags, mode_t mode, const gchar *path_header);

// 文件安全打开,返回文件对象指针,打开文件前会校验文件是否是绝对路径,及是否存在软链接,是否在指定路径下
FILE *fopen_s(const gchar *path, const gchar *mode, const gchar *path_header);

// 文件关闭接口,与open_s配套使用
gint fclose_s(FILE *fp);

// 文件关闭接口,与fopen_s配套使用
gint close_s(gint32 fd);

// 判断文件路径是否有效,是否在指定路径下,一般用于文件导入场景
gint32 check_real_path_s(const gchar *file_path, const gchar *path_header);

// 在文件打开前,对文件路径的校验,判断文件路径是否是绝对路径以及是否存在软链接,是否在指定路径下,适用于文件导出场景
gint32 check_realpath_before_open_s(const gchar *file_path, const gchar *path_header);

// 适用于文件已经打开后,对文件操作前路径的一致性校验,判断文件路径是否是绝对化路径以及是否存在软链接,是否在指定路径下
gint32 check_realpath_after_open_s(const gint32 fd,
                                   const gchar *file_path, const gchar *path_header);

// 支持文件移动(源文件移动到目标文件,保留源文件的内容和属性,源文件删除),仅包括文件内容,打开文件进行路径和软链接检查
gint32 move_file_s(const gchar *src_path, const gchar *dest_path);

// 支持文件完整拷贝接口,包括文件内容、文件属性(属主、创建时间、修改事件、访问时间等),打开文件进行路径和软链接检查
gint32 copy_file_s(const gchar *src_path, const gchar *dest_path);

// 支持文件拷贝接口,仅包括文件内容,打开文件进行路径和软链接检查
gint32 copy_file_content_s(const gchar *src_path, const gchar *dest_path);

// 支持文件读接口,打开文件需要进行路径和软链接检查
gint32 read_file_s(const gchar *file_name, void *buf, size_t size);

// 支持文件读接口,打开文件需要进行路径和软链接检查
gint32 write_file_s(const gchar *file_name, const void *buf, size_t size);

// access函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int access_s(const char *pathname, int mode);

// chdir函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int chdir_s(const char *path);

// opendir函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
DIR *opendir_s(const char *name);

// open函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
gint32 open_s(const gchar *pathname, guint32 flags, mode_t mode, const gchar *path_header);

// pathconf函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
long pathconf_s(const char *path, int name);

// stat函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int stat_s(const char *pathname, struct stat *statbuf);

// chmod函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int chmod_s(const char *pathname, mode_t mode);

// chown函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int chown_s(const char *pathname, uid_t owner, gid_t group);

// execl函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int execl_s(const char *pathname, const char *arg, ...);

// execlp函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int execlp_s(const char *file, const char *arg, ...);

// execle函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int execle_s(const char *pathname, const char *arg, ...);

// execv函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int execv_s(const char *pathname, char *const argv[]);

// execvp函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int execvp_s(const char *file, char *const argv[]);

// link函数的安全版本,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,检验失败返回-1
int link_s(const char *oldpath, const char *newpath);
lua
-- Lua语言版本
-- 文件安全打开,返回文件描述符,打开文件前会校验文件是否是绝对化路径,及是否存在软链接,是否在指定路径下。本函数返回的对象不支持seek方法,在需要使用seek方法指定写入点的特殊场景下,不能使用open_s替代原始的open函数,而是应该在open函数打开前使用check_realpath_before_open_s校验路径
open_s(filename, mode [, path_header])

-- 判断文件路径是否有效,是否在指定路径下,一般用于文件导入的场景
check_real_path_s(file_path [, path_header])

-- 在文件打开前,对文件路径的校验,判断文件路径是否是绝对路径以及是否存在软链接,是否在指定路径下,适用于文件导出场景
check_realpath_before_open_s(file_path [, path_header])

-- 适用于文件已经打开后,对文件操作前路径的一致性校验,判断文件路径是否是绝对化路径以及是否存在软链接,是否在指定路径下
check_realpath_after_open_s([file], file_path [, path_header])

-- 支持文件移动(源文件移动到目标文件,保留源文件的内容和属性,源文件删除),仅包括文件内容,打开文件进行路径和软链接检查
move_file_s(src_path, dest_path)

-- 支持文件完整拷贝接口,包括文件内容、文件属性(属主、创建时间、修改事件、访问时间等),打开文件进行路径和软链接检查
copy_file_s(src_path, dest_path)

-- 支持文件拷贝接口,仅包括文件内容,打开文件进行路径和软链接检查
copy_file_content_s(src_path, dest_path)

-- 支持文件读接口,读取指定字节数据,打开文件需要进行路径和软链接检查
read_file_s(file_name, size)

-- 支持文件写接口,写入指定数据,打开文件需要进行路径和软链接检查
write_file_s(file_name, data)

-- os.execute的安全版本,用于防命令注入执行shell命令,执行前会校验外部输入的字符串是否包含可能导致shell命令注入的特殊字符,检验失败会抛出错误,特殊字符包含"||", ";", "&&", "$", "|", "&", ">>", ">", "<", "`", "\", "!", "\n"
execute_s(cmd)

-- vos.system_s的安全版本,用于执行shell命令,执行前会校验外部输入的字符串是否包含可能导致shell命令注入的特殊字符,检验失败会抛出错误,特殊字符包含"||", ";", "&&", "$", "|", "&", ">>", ">", "<", "`", "\", "!", "\n"
check_before_system_s(cmd_path [, cmdstring])

-- chmod的安全版本,用于修改文件权限,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,校验失败会返回-1
chmod_s(path, fd_mode)

-- chown的安全版本,用于修改文件的属主和属组,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,校验失败会返回-1
chown_s(path, owner, group)

-- chdir的安全版本,用于切换目录,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,校验失败会抛出错误
chdir_s(dir_path)

-- stat的安全版本,用于显示文件或文件系统的详细信息,执行前会判断文件路径是否是绝对化路径以及是否存在软链接,校验失败会抛出错误
stat_s(path)

【案例一】

lua
-- 错误示例:
function export_event(list, path)
   -- 省略部分代码
    utils.remove(path) -- 错误,未对path进行校验
end

-- 正确示例:
function export_event(list, path)
    if check_realpath_before_open_s(path, '/tmp') ~= 0 then
        return -- path是非预期路径,不能直接删除
    end
    utils.remove(path) -- 正确,已对path进行校验
    -- 省略部分代码
end

G.SEC.FIL.B03 解压缩包操作需要进行完备的校验,防止zip炸弹

【规则说明】

解压前需要判断:

  • 压缩包文件路径是否为软链接;
  • 压缩包文件大小;
  • 压缩包内被压缩文件的总数;
  • 解压后的文件总大小;
  • 压缩包内压缩文件名是否包含../(如果使用unzip命令解压,则无需校验../);
  • 解压得到的文件中是否包括软链接。

框架已提供包含上述检查的安全解压接口,在解压操作时使用对应接口进行操作;

  • secure_tar_unzip: 对tar压缩文件格式的压缩包进行解压
  • unzip_s: 对zip压缩文件格式的压缩包进行解压

【案例一】

lua
-- 解压tar格式文件
-- 函数声明:secure_tar_unzip(file_path, dest_path, max_unzip_file_size, max_file_num)
local utils = require 'mc.utils'
local ok = utils.secure_tar_unzip(file_path, dest_path, max_unzip_file_size, max_file_num)
if not ok then
    log:error('secure_tar_unzip failed')
end

【案例二】

lua
-- 解压zip格式文件
-- 函数声明:unzip_s(file_path, dest_path, max_unzip_file_size, max_file_num)
local utils = require 'mc.utils'
local ok = utils.unzip_s(file_path, dest_path, max_unzip_file_size, max_file_num)
if not ok then
    log:error('unzip_s failed')
end

G.SEC.FIL.B04 往同一路径下解压文件,需要做重入判断,禁止允许并发解压

【规则说明】

如果允许同一个路径下并发解压,则可以构造两个特殊的压缩包,利用解压过程中残留的文件,进行任意文件写,可以导致沙箱被破解。

G.SEC.FIL.B05 使用外部导入的文件之前,需判断文件属主是否与执行导入操作的用户一致

【规则说明】

安全要求,导入之前先判断文件属主是否与触发导入操作的用户一致,可以避免高权限用户上传的文件被低权限用户使用。

【案例一】

lua
local function check_owner(ctx, file_path)
    local uid = get_uid_gid_by_name(ctx)   -- 通过上下文获取当前用户的uid
    local file_owner = utils_core.stat_s(file_path).st_uid
    if file_owner ~= uid then
        log:error('file owner(%d) is not match current user(%d)', file_owner, uid)
        utils.remove_file(file_path)
        return false
    end

    return true
end

G.SEC.FIL.B06 高权限用户严禁执行低权限用户可修改的shell脚本

【规则说明】

如果高权限用户执行低权限用户具备写权限的脚本文件,那么低权限用户只要修改脚本内容,就可以在高权限用户执行该脚本的时候,借助高权限用户来执行恶意命令。具体来说,包括以下两种场景:

  • 直接使用:高权限用户直接执行低权限用户可修改的脚本文件;
  • 间接使用:高权限用户执行的是低权限用户不可修改的脚本文件,但是该脚本里面会执行某些低权限用户可修改的脚本文件。

G.SEC.FIL.B07 不要在共享目录中创建和存放临时文件

【规则说明】

共享目录是指其它非特权用户可以访问的目录。程序的临时文件应当是程序自身独享的,任何将自身临时文件置于共享目录的做法,将导致其他共享用户获得该程序的额外信息,产生信息泄露。因此,不要在任何共享目录创建仅由程序自身使用的临时文件。

临时文件通常用于辅助保存不能驻留在内存中的数据或存储临时的数据,也可用作进程间通信的一种手段(通过文件系统传输数据)。例如,一个进程在共享目录中创建一个临时文件,该文件名可能使用了众所周知的名称或者一个临时的名称,然后就可以通过该文件在进程间共享信息。这种通过在共享目录中创建临时文件的方法实现进程间共享的做法很危险,因为共享目录中的这些文件很容易被攻击者劫持或操纵。

临时文件(例如升级包、证书文件等)必须先拷贝到非共享目录(例如从/tmp目录拷贝到/data或者/dev/shm下)再进行校验 。

G.SEC.FIL.B08 建议将远程文件下载到本地目录后再进行处理

【规则说明】

部分远程文件通过将目录挂载后直接在挂载目录上进行操作,此时远程服务器不可控,攻击者可在完成文件校验后竞争替换远程服务器上的文件实现绕过。

4 外部输入

使用外部输入数据时,处理上面指出的命令注入、代码注入、路径跨越风险外,还需要注意以下问题。

G.SEC.EXT.B01 禁止外部输入直接作为循环上限

【规则说明】

以外部输入作为循环上限时,可能会由于范围过大而索引不到结果,引发异常。

G.SEC.EXT.B02 禁止外部输入直接作为除数

【规则说明】

Lua同C语言要求,除法或求余操作中,一旦发生除0的情况,将产生异常。

G.SEC.EXT.B03 外部输入作为表索引时,对索引结果要判空后再使用

【规则说明】

以外部输入作为表索引或者数组下标时,需要对索引结果判空再使用,否则如果对得到的nil值进行操作,可能引发异常。

【案例一】

lua
-- 错误示例
-- 根据cmd_type加载具体的消息处理类
function func(type)
    -- 错误,应该对handlers[cmd_type]判空后再使用,否则传入一个不存在的索引值,会触发程序断言失败
    self.handler_func = handlers[type].func
end

-- 正确示例
function func(type)
    local handler = handlers[type]
    if not handler then
        return
    end
    self.handler_func = handler.func
end

G.SEC.EXT.B04 公共函数中禁止日志打印可能是外部输入的参数取值

【规则说明】

公共函数打印外部输入参数,容易造成信息泄露风险,且代码排查时难以识别。例如,一个检验文件路径是否合法的公共函数中,如果打印了此路径;那么调用者不小心将包含密码的远程文件路径传入了,就会导致敏感信息泄露。

特别要注意一些公共机制回调函数,如配置导入回调不要无条件打印参数值;以及一些开源软件日志配置中,要注意不能打印可能是敏感信息的部分。

G.SEC.EXT.B05 对于外部输入进行直接校验,不要对二次计算后的结果进行间接校验

【规则说明】

对外部输入的运算结果进行校验容易出现整数翻转或整数溢出截断,造成安全风险,因此应当对外部输入本身进行直接校验。

【案例一】

data_cnt是外部消息控制的,在校验报文长度时没有直接校验,而是对一个乘法和加法运算结果进行校验,这就导致攻击者通过构造特殊的data_cnt取值,使得计算结果data_len超过unsigned int的最大范围,出现整数溢出截断成一个较小的数,从而使下面的长度校验能够通过,但是实际使用data_cnt时又会出现越界读写。

c
int HandleMsg(unsigned int data_cnt, unsigned int msg_len)
{
    unsigned int data_len = sizeof(MSG_INFO_S) + data_cnt * sizeof(Data_S);
    // 校验消息长度是否满足要求
    if (data_len > msg_len) {
        return -1;
    }

    for (int i = 0; i < data_cnt; i++) {
        ...... // 省略部分代码
    }
}

5 敏感信息

openUBMC涉及的敏感信息包括但不限于:openUBMC用户密码、会话token、包含密码的远程文件传输URL、包含密码的虚拟媒体挂载地址、SMTP登录密码、SNMP团体名、SNMP加密密码、SP升级的ImageURI和SignalURI、SP系统部署配置中的CDKey和RootPwd、NTP组秘钥、KerberOs密钥表、证书私钥、证书加密密码、加密密钥(包括根密钥、主密钥和工作密钥)、Redfish事件订阅请求头、VNC密码、BIOS密码、LDAP绑定密码(BindDNPsw)、SSH Host key。

G.SEC.SEN.B01 敏感信息明文禁止泄露

【规则说明】

敏感信息泄露行为包括:打印到日志、在接口响应体或错误消息中明文返回、在UI界面上(包括命令行和Web)明文显示、明文存储到文件系统(含内存文件系统)、明文存储到数据库。

G.SEC.SEN.B02 北向接口映射器配置中,对于可能是敏感信息的请求参数,需要增加Sensitive标识

【规则说明】

为了避免在cli命令或者接口错误响应消息中回显用户输入的敏感信息,需要在映射器配置中增加Sensitive标识。增加该标识后,可以自动隐藏敏感信息具体值。

【案例一】

json
{
    "Type": "PATCH",
    "ReqBody": [
        {
            "Name": "SenderPassword",
            "Type": "string",
            "Sensitive": true  // 发送密码是敏感信息,需要在回显中隐藏具体值
        }
    ]
}

G.SEC.SEN.B03 后台组件错误引擎抛错时,禁止在message中返回敏感信息

【规则说明】

后台组件在处理错误引擎抛错时,如果参数值是敏感信息,则不能直接传入抛错函数,而应该使用6个*替代,避免在北向接口回显中打印出来。

【案例一】

lua
-- 错误案例:
if url:len() > 255 then
    error(base_messages.PropertyValueFormatError(url, '%Url')) -- 错误:url可能是敏感信息
end

-- 正确案例:
if url:len() > 255 then
    error(base_messages.PropertyValueFormatError('******', '%Url')) -- 正确:隐藏了敏感信息
end

6 资源泄露

G.SEC.LEA.B01 C库中须保证内存正确释放

【规则说明】

在封装给Lua调用的C/C++动态库中,使用C/C++库函数创建的资源,仍需跟正常C/C++程序一样,在使用完毕后,需要进行资源释放,否则存在资源泄露。

G.SEC.LEA.B02 注意正确调用开源软件的资源申请和释放函数

  • 通过Lua重新封装的开源软件API,在调用时同样要注意资源的申请和释放
  • 比如,若调用了某个封装了SSL_CTX_new的Lua函数,则必须相应地调用封装了SSL_CTX_free的接口

G.SEC.LEA.B03 资源的申请和释放必须成对

以下为openUBMC框架封装的资源申请释放常用Lua接口:

  • 数据库的预处理语句与销毁预处理语句
lua
local vm = db:prepare('SELECT COUNT(*) FROM t_record')
...
vm:finalize()
  • 空表的申请与释放语句
lua
local table_cache = require 'mc.table_cache'
local tbl_cache = table_cache.new()
-- 申请一个空表
local data = tbl_cache:allocate()
...
-- 释放这个表
tbl_cache:deallocate(data)

7 其他

G.SEC.OTH.B01 禁止封装会调用到阻塞性API的C库给Lua代码直接调用

【规则说明】

C库中调用阻塞性API,如sleepusleepacceptrecvsend等,直接在Lua代码中调用该C库函数会阻塞skynet的worker线程,导致该进程下所有skynet服务任务调度出现异常,进而导致相关资源协作接口访问阻塞、组件心跳丢失被框架重启等问题。推荐做法为:调用框架提供的worker库,创建一个worker线程,将阻塞操作在worker线程中执行

G.SEC.OTH.B02 禁止在Lua代码中直接调用执行时间较长的shell脚本

【规则说明】

在Lua代码中直接执行时间较长的shell脚本(如配置网络、压缩/解压缩大文件)也会阻塞skynet的worker线程,影响同G.SEC.OTH.B01。解决方法也是使用框架提供的worker机制来执行shell脚本。

G.SEC.OTH.B03 禁止在app启动函数中执行耗时较长或者耗时不确定的操作

【规则说明】

在app启动函数中执行耗时较长或者耗时不确定的操作(例如阻塞性等待其他组件上资源协作接口),可能导致启动超时,服务被框架重启。此类操作建议异步执行,不阻塞启动过程。

G.SEC.OTH.B04 创建skynet协程操作外部可控时,必须控制协程创建的最大数量

【规则说明】

同时创建过多的skynet协程,会导致app内存暴涨直至出现OOM(Out Of Memery),如果协程创建操作外部可控,则会造成DOS攻击,因此必须限制最大并发数量。

G.SEC.OTH.B05 如果某个外部可控的操作存在较多内存分配,则须保证及时GC

【规则说明】

某个外部可控的操作存在较多内存分配,例如查询BIOS配置、查询BIOS属性注册表、导入根证书等,可以考虑在操作结束后主动进行GC,避免因为Lua虚拟机自动GC不及时导致OOM。

添加collectgarbage调用会引起CPU占用升高,请在sig例会评审通过后再添加

G.SEC.OTH.B06 使用libcurl发起的请求,必须显式设置超时时间和连接超时时间,避免网络异常情况下线程阻塞

c
\* 示例 *\
CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); // 连接超时时间10s
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L); // 设置超时时间60s
curl_easy_perform(curl);