21、基础 | 程序员必看的525钟C++重点
《360 安全规则集合》简称《安规集》,是一套详细的 C/C++ 安全编程指南,由 360 集团质量工程部编著,将编程时需要注意的问题总结成若干规则,可为制定编程规范提供依据,也可为代码审计或相关培训提供指导意见,旨在提升软件产品的可靠性、健壮性、可移植性以及可维护性,从而提升软件产品的综合安全性能。
C/C++ 安全规则集合
[外链图片转存中...(img-z2rxUD7I-1720615338134)]
Bjarne Stroustrup: “C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off.”
针对 C 和 C++ 语言,本文收录了 525 种需要重点关注的问题,可为制定编程规范提供依据,也可为代码审计以及相关培训提供指导意见,适用于桌面、服务端以及嵌入式等软件系统。
每个问题对应一条规则,每条规则可直接作为规范条款或审计检查点,本文是适用于不同应用场景的规则集合,读者可根据自身需求从中选取某个子集作为规范或审计依据,从而提高软件产品的安全性。
规则说明
规则按如下主题分为 17 个类别:
- Security:敏感信息防护与安全策略
- Resource:资源管理
- Precompile:预处理、宏、注释、文件
- Global:全局及命名空间作用域
- Type:类型设计与实现
- Declaration:声明
- Exception:异常
- Function:函数实现
- Control:流程控制
- Expression:表达式
- Literal:字面常量
- Cast:类型转换
- Buffer:缓冲区
- Pointer:指针
- Interruption:中断与信号处理
- Concurrency:异步与并发
- Style:样式与风格
每条规则包括:
- 编号:规则在本文中的章节编号,以“R”开头,称为 Section-ID
- 名称:用简练的短语描述违反规则的状况,以“ID_”开头,称为 Fault-ID
- 标题:规则的定义
- 说明:规则设立的原因、违反规则的后果、示例、改进建议、参照依据、参考资料等内容
如果违反规则,后果的严重程度分为:
- Error:直接导致错误或形成安全漏洞
- Warning:可导致错误或形成安全隐患
- Suspicious:可疑的代码,需进一步排查
- Suggestion:代码质量降低,应依照建议改进
规则的说明包含:
- 示例:规则相关的示例代码,指明符合规则(Compliant)的和违反规则(Non-compliant)的情况
- 相关:与当前规则有相关性的规则,可作为扩展阅读的线索
- 依据:规则依照的 ISO/IEC 标准条目,C 规则以 ISO/IEC 9899:2011 为主,C++ 规则以 ISO/IEC 14882:2011 为主
- 配置:某些规则的细节可灵活设置,审计工具可以此为参照实现定制化功能
- 参考:规则参考的其他规范条目,如 C++ Core Guidelines、MISRA、SEI CERT Coding Standards 等,也可作为扩展阅读的线索
规则的相关性分为:
- 特化:设规则 A 的特殊情况需要由规则 B 阐明,称规则 B 是规则 A 的特化
- 泛化:与特化相反,称规则 A 是规则 B 的泛化
- 相交:设两个规则针对不同的问题,但在内容上有一定的交集,称这两个规则相交
规则以“标准名称:版本 章节编号(段落编号)-性质
”的格式引用标准,如“ISO/IEC 14882:2011 5.6(4)-undefined
”,表示引用 C++11 标准的第 5 章第 6 节第 4 段说明的具有 undefined 性质的问题。
其中“性质”分为:
- undefined:可使程序产生未定义的行为,这种行为造成的后果是不可预期的
- unspecified:可使程序产生未声明的行为,这种行为由编译器或环境定义,具有随意性
- implementation:可使程序产生由实现定义的行为,这种行为由编译器或环境定义,有明确的文档支持
- deprecated:已被废弃的或不建议继续使用的编程方式
本文以 ISO/IEC 9899:2011、ISO/IEC 14882:2011 为主要依据,兼顾 C++17 以及历史标准,没有特殊说明的规则同时适用于 C 语言和 C++ 语言,只适用于某一种语言的规则会另有说明。
规则选取
本文是适用于不同应用场景的规则集合,读者可选取适合自己需求的规则。
指出某种错误的规则,如有“不可”、“不应”等字样的规则应尽量被选取,有“禁用”等字样的规则可能只适用于某一场景,可酌情选取。
如果将本文作为培训内容,为了全面理解各种场景下存在的问题,应选取全部规则。
规则列表
- R1.1 敏感数据不可写入代码
- R1.2 敏感数据不可被系统外界感知
- R1.3 敏感数据在使用后应被有效清理
- R1.4 公共成员或全局对象不应记录敏感数据
- R1.5 与内存空间布局相关的信息不可被外界感知
- R1.6 与网络地址相关的信息不应写入代码
- R1.7 预判用户输入造成的不良后果
- R1.8 对资源设定合理的访问权限
- R1.9 对用户落实有效的权限管理
- R1.10 避免引用危险符号名称
- R1.11 避免使用危险接口
- R1.12 避免使用已过时的接口
- R1.13 避免除 0 等计算异常
- R1.14 选择安全的异常处理方式
- R1.15 不应产生或依赖未定义的行为
- R1.16 不应依赖未声明的行为
- R1.17 避免依赖由实现定义的行为
- R1.18 保证组件的可靠性
- R1.19 保证第三方软件的可靠性
- R1.20 隔离非正式功能的代码
- R1.21 启用平台和编译器提供的防御机制
- R1.22 禁用不安全的编译选项
- R2.1 不可失去对已分配资源的控制
- R2.2 不可失去对已分配内存的控制
- R2.3 不可访问未初始化或已释放的资源
- R2.4 使资源接受对象化管理
- R2.5 资源的分配与回收方法应成对提供
- R2.6 资源的分配与回收方法应配套使用
- R2.7 不应在模块之间传递容器类对象
- R2.8 不应在模块之间传递非标准布局类型的对象
- R2.9 对象申请的资源应在析构函数中释放
- R2.10 对象被移动后应重置状态再使用
- R2.11 构造函数抛出异常需避免相关资源泄漏
- R2.12 不可重复释放资源
- R2.13 用 delete 释放对象需保证其类型完整
- R2.14 用 delete 释放对象不可多写中括号
- R2.15 用 delete 释放数组不可漏写中括号
- R2.16 不可释放非动态分配的内存
- R2.17 在一个表达式语句中最多使用一次 new
- R2.18 流式资源对象不应被复制
- R2.19 避免使用变长数组
- R2.20 避免使用在栈上动态分配内存的函数
- R2.21 局部数组不应过大
- R2.22 避免不必要的内存分配
- R2.23 避免分配大小为零的内存空间
- R2.24 避免动态内存分配
- R2.25 判断资源分配函数的返回值是否有效
- R2.26 在 C++ 代码中禁用 C 资源管理函数
- R4.1 全局名称应遵循合理的命名方式
- R4.2 为代码设定合理的命名空间
- R4.3 main 函数只应位于全局作用域中
- R4.4 不应在头文件中使用 using directive
- R4.5 不应在头文件中使用静态声明
- R4.6 不应在头文件中定义匿名命名空间
- R4.7 不应在头文件中实现函数或定义对象
- R4.8 不应在匿名命名空间中使用静态声明
- R4.9 全局对象的初始化不可依赖未初始化的对象
- R4.10 全局对象只应为常量或静态对象
- R4.11 全局对象只应为常量
- R4.12 全局对象不应同时被 static 和 const 等关键字限定
- R4.13 全局及命名空间作用域中禁用 using directive
- R4.14 避免无效的 using directive
- R4.15 不应定义全局 inline 命名空间
- R4.16 不可修改 std 命名空间
- 5.1 Class
- R5.1.1 类的非常量数据成员均应为 private
- R5.1.2 类的非常量数据成员不应定义为 protected
- R5.1.3 类不应既有 public 数据成员又有 private 数据成员
- R5.1.4 有虚函数的基类应具有虚析构函数
- R5.1.5 避免多重继承自同一非虚基类
- R5.1.6 存在析构函数或拷贝赋值运算符时,不应缺少拷贝构造函数
- R5.1.7 存在拷贝构造函数或析构函数时,不应缺少拷贝赋值运算符
- R5.1.8 存在拷贝构造函数或拷贝赋值运算符时,不应缺少析构函数
- R5.1.9 存在任一拷贝、移动、析构相关的函数时,应定义所有相关函数
- R5.1.10 避免重复实现由默认拷贝、移动、析构函数完成的功能
- R5.1.11 可接受一个参数的构造函数需用 explicit 关键字限定
- R5.1.12 重载的类型转换运算符需用 explicit 关键字限定
- R5.1.13 不应过度使用 explicit 关键字
- R5.1.14 带模板的赋值运算符不应与拷贝或移动赋值运算符混淆
- R5.1.15 带模板的构造函数不应与拷贝或移动构造函数混淆
- R5.1.16 抽象类禁用拷贝和移动赋值运算符
- R5.1.17 数据成员的数量应在规定范围之内
- R5.1.18 数据成员之间的填充数据不应被忽视
- R5.1.19 常量成员函数不应返回数据成员的非常量指针或引用
- R5.1.20 类成员应按 public、protected、private 的顺序声明
- R5.1.21 POD 类和非 POD 类应分别使用 struct 和 class 关键字定义
- R5.1.22 继承层次不应过深
- 5.2 Enum
- 5.3 Union
- 6.1 Naming
- 6.2 Qualifier
- R6.2.1 const、volatile 不应重复
- R6.2.2 const、volatile 限定指针类型的别名是可疑的
- R6.2.3 const、volatile 不可限定引用
- R6.2.4 const、volatile 限定类型时的位置应统一
- R6.2.5 const、volatile 等关键字不应出现在基本类型名称的中间
- R6.2.6 指向常量字符串的指针应使用 const 声明
- R6.2.7 声明枚举类型的底层类型时不应使用 const 或 volatile
- R6.2.8 对常量的定义不应为引用
- R6.2.9 禁用 restrict 指针
- R6.2.10 非适当场景禁用 volatile
- R6.2.11 相关对象未被修改时应使用 const 声明
- 6.3 Specifier
- R6.3.1 合理使用 auto 关键字
- R6.3.2 不应使用已过时的关键字
- R6.3.3 不应使用多余的 inline 关键字
- R6.3.4 extern 关键字不应作用于类成员的声明或定义
- R6.3.5 重写的虚函数应声明为 override 或 final
- R6.3.6 override 和 final 关键字不应同时出现在声明中
- R6.3.7 override 或 final 关键字不应与 virtual 关键字同时出现在声明中
- R6.3.8 不应将 union 设为 final
- R6.3.9 未访问 this 指针的成员函数应使用 static 声明
- R6.3.10 声明和定义内部链接的对象和函数时均应使用 static 关键字
- R6.3.11 inline、virtual、static、typedef 等关键字的位置应统一
- 6.4 Declarator
- 6.5 Parameter
- 6.6 Initializer
- 6.7 Object
- 6.8 Function
- R6.8.1 派生类不应重新定义与基类相同的非虚函数
- R6.8.2 重载运算符的返回类型应与内置运算符相符
- R6.8.3 赋值运算符应返回所属类的非 const 左值引用
- R6.8.4 拷贝构造函数的参数应为同类对象的 const 左值引用
- R6.8.5 拷贝赋值运算符的参数应为同类对象的 const 左值引用
- R6.8.6 移动构造函数的参数应为同类对象的非 const 右值引用
- R6.8.7 移动赋值运算符的参数应为同类对象的非 const 右值引用
- R6.8.8 不应重载取地址运算符
- R6.8.9 不应重载逗号运算符
- R6.8.10 不应重载“逻辑与”和“逻辑或”运算符
- R6.8.11 拷贝和移动赋值运算符不应为虚函数
- R6.8.12 比较运算符不应为虚函数
- R6.8.13 final 类中不应声明虚函数
- 6.9 Bitfield
- 6.10 Complexity
- 6.11 Old-style
- 6.12 Other
- R7.1 保证异常安全
- R7.2 处理所有异常
- R7.3 不应抛出过于宽泛的异常
- R7.4 不应捕获过于宽泛的异常
- R7.5 不应抛出非异常类型的对象
- R7.6 不应捕获非异常类型的对象
- R7.7 全局对象的初始化过程不可抛出异常
- R7.8 析构函数不可抛出异常
- R7.9 内存回收函数不可抛出异常
- R7.10 对象交换过程不可抛出异常
- R7.11 移动构造函数和移动赋值运算符不可抛出异常
- R7.12 异常类的拷贝构造函数不可抛出异常
- R7.13 异常类的构造函数和异常信息相关的函数不应抛出异常
- R7.14 与标准库相关的 hash 过程不应抛出异常
- R7.15 由 noexcept 标记的函数不可产生未处理的异常
- R7.16 避免异常类多重继承自同一非虚基类
- R7.17 通过引用捕获异常
- R7.18 捕获异常时不应产生对象切片问题
- R7.19 捕获异常后不应直接再次抛出异常
- R7.20 重新抛出异常时应使用空 throw 表达式(throw;)
- R7.21 不应在 catch 子句外使用空 throw 表达式(throw;)
- R7.22 不应抛出指针
- R7.23 不应抛出 NULL
- R7.24 不应抛出 nullptr
- R7.25 不应在模块之间传播异常
- R7.26 禁用动态异常说明
- R7.27 禁用 C++ 异常
- R8.1 main 函数的返回类型只应为 int
- R8.2 main 函数不应被调用、重载或被 inline、static 等关键字限定
- R8.3 参数名称在声明处和实现处应保持一致
- R8.4 多态类的对象作为参数时不应采用值传递的方式
- R8.5 不应存在未被使用的具名形式参数
- R8.6 形式参数不应被修改
- R8.7 复制成本高的参数不应按值传递
- R8.8 转发引用只应作为 std::forward 的参数
- R8.9 局部对象在使用前应被初始化
- R8.10 成员须在声明处或构造时初始化
- R8.11 基类对象构造完毕之前不可调用成员函数
- R8.12 在面向构造或析构函数体的 catch 子句中不可访问非静态成员
- R8.13 成员初始化应遵循声明的顺序
- R8.14 在构造函数中不应使用动态类型
- R8.15 在析构函数中不应使用动态类型
- R8.16 在析构函数中避免调用 exit 函数
- R8.17 拷贝构造函数应避免实现复制之外的功能
- R8.18 移动构造函数应避免实现数据移动之外的功能
- R8.19 拷贝赋值运算符应处理参数是自身对象时的情况
- R8.20 不应存在无效的写入操作
- R8.21 不应存在没有副作用的语句
- R8.22 不应存在得不到执行机会的代码
- R8.23 有返回值的函数其所有分枝都应显式返回
- R8.24 不可返回局部对象的地址或引用
- R8.25 不可返回临时对象的地址或引用
- R8.26 合理设置 lambda 表达式的捕获方式
- R8.27 函数返回值不应为右值引用
- R8.28 函数返回值不应为常量对象
- R8.29 函数返回值不应为基本类型的常量
- R8.30 被返回的表达式应与函数的返回类型一致
- R8.31 被返回的表达式不应为相同的常量
- R8.32 具有 noreturn 属性的函数不应返回
- R8.33 具有 noreturn 属性的函数返回类型只应为 void
- R8.34 由 atexit、at_quick_exit 指定的处理函数应正常返回
- R8.35 函数模板不应被特化
- R8.36 函数的退出点数量应在规定范围之内
- R8.37 函数的标签数量应在规定范围之内
- R8.38 函数的行数应在规定范围之内
- R8.39 lambda 表达式的行数应在规定范围之内
- R8.40 函数参数的数量应在规定范围之内
- R8.41 不应定义复杂的内联函数
- R8.42 避免函数调用自身
- R8.43 不可递归调用析构函数
- R8.44 作用域及类型嵌套不应过深
- R8.45 汇编代码不应与普通代码混合
- R8.46 避免重复的函数实现
- 9.1 If
- R9.1.1 if 语句不应被分号隔断
- R9.1.2 在 if...else-if 分枝中不应有重复的条件
- R9.1.3 在 if...else-if 分枝中不应有被遮盖的条件
- R9.1.4 if 分枝和 else 分枝的代码不应完全相同
- R9.1.5 if...else-if 各分枝的代码不应完全相同
- R9.1.6 if 分枝和隐含的 else 分枝代码不应完全相同
- R9.1.7 没有 else 子句的 if 语句与其后续代码相同是可疑的
- R9.1.8 if 分枝和 else 分枝的起止语句不应相同
- R9.1.9 if 语句作用域的范围不应有误
- R9.1.10 如果 if 关键字前面是右大括号,if 关键字应另起一行
- R9.1.11 if 语句的条件不应为赋值表达式
- R9.1.12 if 语句不应为空
- R9.1.13 if...else-if 分枝数量应在规定范围之内
- R9.1.14 if 分枝中的语句应该用大括号括起来
- R9.1.15 所有 if...else-if 分枝都应以 else 子句结束
- 9.2 For
- 9.3 While
- 9.4 Do
- 9.5 Switch
- R9.5.1 switch 语句不应被分号隔断
- R9.5.2 switch 语句不应为空
- R9.5.3 case 标签的值不可超出 switch 条件的范围
- R9.5.4 switch 语句中任何子句都应从属于某个 case 或 default 分枝
- R9.5.5 case 和 default 标签应直接从属于 switch 语句
- R9.5.6 不应存在紧邻 default 标签的空 case 标签
- R9.5.7 不应存在内容完全相同的 case 分枝
- R9.5.8 switch 语句的条件不应为 bool 型
- R9.5.9 switch 语句不应只包含 default 标签
- R9.5.10 switch 语句不应只包含一个 case 标签
- R9.5.11 switch 语句分枝数量应在规定范围之内
- R9.5.12 switch 语句应配有 default 分枝
- R9.5.13 switch 语句的每个非空分枝都应该用无条件的 break 或 return 语句终止
- R9.5.14 switch 语句应该用大括号括起来
- R9.5.15 switch 语句不应嵌套
- 9.6 Try-catch
- 9.7 Jump
- 10.1 Logic
- 10.2 Evaluation
- R10.2.1 不可依赖不会生效的副作用
- R10.2.2 不可依赖未声明的求值顺序
- R10.2.3 在表达式中不应多次读写同一对象
- R10.2.4 bool 对象不应参与位运算、大小比较、数值增减
- R10.2.5 枚举对象不应参与位运算或算数运算
- R10.2.6 参与数值运算的 char 对象应显式声明 signed 或 unsigned
- R10.2.7 signed char 和 unsigned char 对象只应用于数值计算
- R10.2.8 不应将 NULL 当作整数使用
- R10.2.9 运算结果不应溢出
- R10.2.10 移位数量不应超过相关类型比特位的数量
- R10.2.11 按位取反需避免由类型提升产生的多余数据
- R10.2.12 逗号表达式的子表达式应具有必要的副作用
- R10.2.13 自增、自减表达式不应作为子表达式
- 10.3 Operator
- 10.4 Assignment
- 10.5 Comparison
- 10.6 Call
- R10.6.1 不应忽略重要的返回值
- R10.6.2 不可臆断返回值的意义
- R10.6.3 避免对象切片
- R10.6.4 避免显式调用析构函数
- R10.6.5 不应将非 POD 对象传入可变参数列表
- R10.6.6 C 格式化字符串需要的参数个数与实际传入的参数个数应一致
- R10.6.7 C 格式化占位符与其对应参数的类型应一致
- R10.6.8 C 格式化字符串应为常量
- R10.6.9 在 C++ 代码中禁用 C 字符串格式化方法
- R10.6.10 形参与实参均为数组时,数组大小应一致
- R10.6.11 禁用不安全的字符串函数
- R10.6.12 禁用 atof、atoi、atol 以及 atoll 等函数
- R10.6.13 合理使用 std::move
- R10.6.14 合理使用 std::forward
- 10.7 Sizeof
- 10.8 Assertion
- 10.9 Complexity
- 10.10 Other
- R11.1 转义字符的反斜杠不可误写成斜杠
- R11.2 在字符常量中用转义字符表示制表符和控制字符
- R11.3 在字符串常量中用转义字符表示制表符和控制字符
- R11.4 8 进制或 16 进制转义字符不应与其他字符连在一起
- R11.5 不应使用非标准转义字符
- R11.6 不应连接不同前缀的字符串常量
- R11.7 字符串常量中不应存在拼写错误
- R11.8 常量后缀由应由大写字母组成
- R11.9 无符号整数常量应具有后缀 U
- R11.10 不应使用非标准常量后缀
- R11.11 禁用 8 进制常量
- R11.12 小心遗漏逗号导致的非预期字符串连接
- R11.13 不应存在 magic number
- R11.14 不应存在 magic string
- R11.15 不应使用多字符常量
- R11.16 合理使用数字分隔符
- R12.1 避免类型转换造成数据丢失
- R12.2 避免数据丢失造成类型转换失效
- R12.3 避免有符号整型与无符号整型相互转换
- R12.4 不应将负数转为无符号数
- R12.5 避免与 void* 相互转换
- R12.6 避免向下类型转换
- R12.7 指针与整数不应相互转换
- R12.8 类型转换不应去掉 const、volatile 等属性
- R12.9 不应转换无继承关系的指针或引用
- R12.10 不应转换无 public 继承关系的指针或引用
- R12.11 非 POD 类型的指针与基本类型的指针不应相互转换
- R12.12 不同的字符串类型之间不可直接转换
- R12.13 避免向对齐要求更严格的指针转换
- R12.14 避免转换指向数组的指针
- R12.15 避免转换函数指针
- R12.16 向下动态类型转换应使用 dynamic_cast
- R12.17 判断 dynamic_cast 转换是否成功
- R12.18 不应转换 new 表达式的类型
- R12.19 不应存在多余的类型转换
- R12.20 可用其他方式完成的转换不应使用 reinterpret_cast
- R12.21 合理使用 reinterpret_cast
- R12.22 在 C++ 代码中禁用 C 风格类型转换
- R13.1 避免缓冲区溢出
- R13.2 为缓冲区分配足够的空间
- R13.3 确保字符串以空字符结尾
- R13.4 memset 等函数不应作用于非 POD 对象
- R13.5 memset 等函数长度相关的参数不应有误
- R13.6 memset 等函数填充值相关的参数不应有误
- R14.1 避免空指针解引用
- R14.2 注意逻辑表达式内的空指针解引用
- R14.3 不可解引用未初始化的指针
- R14.4 不可解引用已失效的指针
- R14.5 避免指针运算的结果溢出
- R14.6 未指向同一数组的指针不可相减
- R14.7 未指向同一数组或同一对象的指针不可比较大小
- R14.8 未指向数组元素的指针不应与整数加减
- R14.9 避免无效的空指针检查
- R14.10 不应重复检查指针是否为空
- R14.11 不应使用非零常量对指针赋值
- R14.12 不应使用常量 0 表示空指针
- R14.13 在 C++ 代码中 NULL 和 nullptr 不应混用
- R14.14 在 C++ 代码中用 nullptr 代替 NULL
- R14.15 不应使用 false 对指针赋值
- R14.16 不应使用 '\0' 等字符常量对指针赋值
- R14.17 指针不应与 false 比较大小
- R14.18 指针不应与 '\0' 等字符常量比较大小
- R14.19 指针与空指针不应比较大小
- R14.20 不应判断 this 指针是否为空
- R14.21 禁用 delete this
- R14.22 释放指针后应将指针赋值为空或其他有效值
- R14.23 函数取地址时应显式使用 & 运算符
- R14.24 指针与整数的加减运算应使用数组下标的方式
- R15.1 避免异步信号处理产生的数据竞争
- R15.2 在异步信号处理函数中避免使用非异步信号安全函数
- R15.3 SIGFPE、SIGILL、SIGSEGV 等信号的处理函数不可返回
- R15.4 禁用 signal 函数
- R15.5 信号处理函数应为 POF
- R16.1 访问共享数据应遵循合理的同步机制
- R16.2 避免在事务中通过路径多次访问同一文件
- R16.3 避免在事务中多次非同步地访问原子对象
- R16.4 避免死锁
- R16.5 避免异步终止线程
- R16.6 避免异步终止共享对象的生命周期
- R16.7 避免虚假唤醒造成同步错误
- R16.8 避免并发访问位域造成的数据竞争
- R16.9 多线程环境中不可使用 signal 函数
- R17.1 遵循统一的代码编写风格
- R17.2 遵循统一的命名风格
- R17.3 遵循统一的空格风格
- R17.4 遵循统一的大括号风格
- R17.5 遵循统一的缩进风格
- R17.6 避免多余的括号
- R17.7 避免多余的分号
1. Security
▌R1.1 敏感数据不可写入代码
ID_plainSensitiveInfo :shield: security warning
代码中的敏感数据极易泄露,产品及相关运维、测试工具的代码均不可记录任何敏感数据。
示例:
/**
* My name is Rabbit
* My passphrase is Y2Fycm90 // Non-compliant
*/
#define PASSWORD "Y2Fycm90" // Non-compliant
const char* passcode = "Y2Fycm90"; // Non-compliant
将密码等敏感数据写入代码是非常不安全的,即使例中 Y2Fycm90 是实际密码的某种变换,聪明的读者也会很快将其破解。
敏感数据的界定是产品设计的重要环节。对具有高可靠性要求的客户端软件,不建议保存任何敏感数据,对于必须保存敏感数据的软件系统,则需要落实安全的存储机制以及相关的评审与测试。
相关
ID_secretLeak
参考
CWE-259
CWE-798
SEI CERT MSC41-C
▌R1.2 敏感数据不可被系统外界感知
ID_secretLeak :shield: security warning
敏感数据出入软件系统时需采用有效的保护措施。
示例:
void foo(User* u) {
log("username: %s, password: %s", u->name, u->pw); // Non-compliant
}
显然,将敏感数据直接输出到界面、日志或其他外界可感知的介质中是不安全的,需避免敏感数据的有意外传,除此之外,还需要落实具体的保护措施。
保护措施包括但不限于:
- 避免用明文或弱加密方式传输敏感数据
- 避免敏感数据从内存交换到外存
- 避免敏感数据写入内存转储文件
- 应具备反调试机制,使外界无法获得程序的内部数据
- 应具备反注入机制,使外界无法篡改程序的行为
下面以 Windows 平台为例,给出阻止敏感数据从内存交换到外存的示例:
class SecretBuf {
size_t len = 0;
unsigned char* buf = nullptr;
public:
SecretBuf(size_t size) {
auto* tmp = (unsigned char*)VirtualAlloc(
0, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE
);
if (VirtualLock(tmp, size)) { // The key point
buf = tmp;
len = size;
} else {
VirtualFree(tmp, 0, MEM_RELEASE);
}
}
~SecretBuf() {
SecureZeroMemory(buf, len); // Clear the secret content
VirtualUnlock(buf, len);
VirtualFree(buf, 0, MEM_RELEASE);
len = 0;
buf = nullptr;
}
size_t size() const { return len; }
unsigned char* ptr() { return buf; }
const unsigned char* ptr() const { return buf; }
};
例中 SecretBuf 是一个缓冲区类,其申请的内存会被锁定在物理内存中,不会与外存交换,可在一定程度上防止其他进程的恶意嗅探,保障缓冲区内数据的安全。SecretBuf 在构造函数中通过 VirtualLock 锁定物理内存,在析构函数中通过 VirtualUnlock 解除锁定,解锁之前有必要清除数据,否则解锁之后残留数据仍有可能被交换到外存,进一步可参见 ID_unsafeCleanup。
SecretBuf 的使用方法如下:
void foo() {
SecretBuf buf(256);
if (buf.ptr()) {
.... // Do something secret using buf.ptr()
} else {
.... // Handle memory error
}
}
在 Linux 等系统中可参见如下有相似功能的接口:
int mlock(const void* addr, size_t len); // In <sys/mman.h>
int munlock(const void* addr, size_t len);
int mlockall(int flags);
int munlockall(void);
相关
ID_unsafeCleanup
参考
CWE-528
CWE-591
SEI CERT MEM06-C
▌R1.3 敏感数据在使用后应被有效清理
ID_unsafeCleanup :shield: security warning
及时清理不再使用的敏感数据是重要的安全措施,且应保证清理过程不会因为编译器的优化而失效。
程序会反复利用内存,敏感数据可能会残留在未初始化的对象或对象之间的填充数据中,如果被存储到磁盘或传输到网络就会造成敏感信息的泄露,可参见 ID_secretLeak 和 ID_ignorePaddingData 的进一步讨论。
示例:
void foo() {
char password[8] = {};
....
memset(password, 0, sizeof(password)); // Non-compliant
}
示例代码调用 memset 覆盖敏感数据以达到清理目的,然而保存敏感信息的 password 为局部数组且 memset 之后没有再被引用,根据相关标准,编译器可将 memset 过程去掉,使敏感数据没有得到有效清理。C11 提供了 memset_s 函数以避免这种问题,某些平台和库也提供了相关支持,如 SecureZeroMemory、explicit_bzero、OPENSSL_cleanse 等不会被优化掉的函数。
在 C++ 代码中,可用 volatile 限定相关数据以避免编译器的优化,再用 std::fill_n 等方法清理,如:
void foo() {
char password[8] = {};
....
volatile char v_padding = 0;
volatile char* v_address = password;
std::fill_n(v_address, sizeof(password), v_padding); // Compliant
}
相关
ID_secretLeak
ID_ignorePaddingData
依据
ISO/IEC 9899:1999 5.1.2.3(3)
ISO/IEC 9899:2011 5.1.2.3(4)
ISO/IEC 9899:2011 K.3.7.4.1
参考
CWE-14
CWE-226
CWE-244
CWE-733
SEI CERT MSC06-C
▌R1.4 公共成员或全局对象不应记录敏感数据
ID_sensitiveName :shield: security warning
公共成员、全局对象可被外部代码引用,如果存有敏感数据则可能会被误用或窃取。
示例:
extern string password; // Non-compliant
struct A {
string username;
string password; // Non-compliant
};
至少应将相关成员改为 private:
class A {
public:
.... // Interfaces for accessing passwords safely
private:
string username;
string password; // Compliant
};
敏感数据最好对引用者完全隐藏,避免被恶意分析、复制或序列化。使数据与接口进一步分离,可参见“Pimpl idiom”等模式。
参考
CWE-766
▌R1.5 与内存空间布局相关的信息不可被外界感知
ID_addressExposure :shield: security warning
函数、对象、缓冲区的地址以及相关内存区域的长度等信息不可被外界感知,否则会成为攻击者的线索。
示例:
int foo(int* p, int n) {
if (n >= some_value) {
log("buffer address: %p, size: %d", p, n); // Non-compliant
}
}
示例代码将缓冲区的地址和长度输出到日志是不安全的,这种代码多以调试为目的,不应将其编译到产品的正式版本中。
相关
ID_bufferOverflow
参考
CWE-200
▌R1.6 与网络地址相关的信息不应写入代码
ID_hardcodedIP :shield: security warning
在代码中记录网络地址不利于维护和移植,也容易暴露产品的网络结构,属于安全隐患。
示例:
string host = "10.16.25.93"; // Non-compliant
foo("172.16.10.36:8080"); // Non-compliant
bar("https://192.168.73.90"); // Non-compliant
应从配置文件中获取地址,并配以加密措施:
MyConf cfg;
string host = cfg.host(); // Compliant
foo(cfg.port()); // Compliant
bar(cfg.url()); // Compliant
特殊的 IP 地址可不受本规则限制,如:
0.0.0.0
255.255.255.255
127.0.0.1-127.255.255.255
相关
ID_addressExposure
▌R1.7 预判用户输入造成的不良后果
ID_hijack :shield: security warning
须对用户输入的脚本、路径、资源请求等信息进行预判,对产生不良后果的输入予以拒绝。
示例:
Result foo() {
return sqlQuery(
"select * from db where key='%s'", userInput() // Non-compliant
);
}
设 userInput 返回用户输入的字符串,sqlQuery 将用户输入替换格式化占位符后执行 SQL 语句,如果用户输入“xxx' or 'x'='x”一类的字符串则相当于执行的是“select * from db where key='xxx' or 'x'='x'”,一个恒为真的条件使 where 限制失效,造成所有数据被返回,这是一种常见的攻击方式,称为“SQL 注入(SQL injection)”,对于 XPath、XQuery、LDAP 等脚本均需考虑这种问题,应在执行前判断用户输入的安全性。
又如:
string bar() {
return readFile(
"/myhome/mydata/" + userInput() // Non-compliant
);
}
这段代码意在将用户输入的路径限制在 /myhome/mydata 目录下,然而这么做是不安全的,如果用户输入带有“../”这种相对路径,则仍可绕过限制,这也是一种常见的攻击方式,称为“路径遍历(directory traversal)”,应在读取文件之前判断路径的安全性。
注意,“用户输入”不单指人的手工输入,源自环境变量、配置文件以及其他软硬件的输入均在此范围内。
参考
CWE-23
CWE-73
CWE-89
CWE-943
▌R1.8 对资源设定合理的访问权限
ID_unlimitedAuthority :shield: security warning
对
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。