[译]CERT Secure Coding Standard — C语言安全编程规范(1)

Posted by c4pr1c3 on February 4, 2011

译序

看完cert的C安全编程规范已经有2个多月了,我在阅读的过程中顺手把官方文档的主要目录翻译了一 下,其中包含了171条“建议”和106条“规则”,个人认为把这些作为一个C语言安全编程规范的cheatsheet是相当不错的。

CERT Secure Coding Standard Statistics

说明:

1.关于“ 建议(Recommendation)”:可以理解为“可选”的规范,一般是和最终软件产品的安全需求有关。遵循“建议”有助于改进系统安全性。

2.关于“规则(Rules)”:可以理解为“强制”的规范。违反“规则”的编码实践有可能导致产生“可被利用的漏洞”。编程实践与该规则的一致性 可以通过自动化分析、形式化方法或者手工检测技术验证。“规则”是确保使用C语言所开发的软件系统安全性的必要条件但非充分条件)。

  1. 规则编号规则:
  • 前3个字母标示规范所属的章节。
  • 2位数字取值范围:00-99。其中00-29保留用于“建议”,30-99保留用于“规则”。
  • -后面的字母表示编程语言,如C。
  1. 部分规则/建议是有”例外“的,需要特别注意。
Section Recommendations Rules
PRE 13 3
DCL 20 9
EXP 19 10
INT 18 6
FLP 6 8
ARR 3 9
STR 11 9
MEM 13 6
FIO 18 16
ENV 5 3
SIG 3 5
ERR 8 3
API 8
MSC 21 7
POS 5 12
Recommendations Rules
TOTAL 171 106

总目录

00. 概述
01. 预处理 (PRE)
02.声明和初始化 (DCL)
03. 表达式 (EXP)
04. 整数 (INT)
05. 浮点数 (FLP)
06. 数组 (ARR)
07. 字符(数组)和字符串 (STR)
08. 内存管理 (MEM)
09. 输入输出 (FIO)
10. 环境 (ENV)
11. 信号 (SIG)
12. 错误处理 (ERR)
13. 应用程序编程接口(API)
14. 并发 (CON)
49. 杂项 (MSC)
50. POSIX (POS)
AA. 参考文献
BB. 定义
CC. 未定义行为
DD. 未确定行为

01. 预处理 (PRE)

</tr> </tbody> </table>

02. 声明和初始化

规则/建议条目全称 例外 笔记 /备注/点评
PRE00-C. 优先使用内联或静态函数而不是函数式宏

PRE00-EX1: 可以用宏实现局部函数(重复出现的代码片断可以访问所包围作用域范围内的自动变量),但无法用内联函数实现

PRE00-EX2: 宏可以用于进行标识符连接或字符串拼接操作

PRE00-EX3: 宏可以用于产生编译时常量,使用内联函数则不能保证一定产生的是编译时常量

PRE00-EX4: 宏可以“模拟”实现”泛型”函数,类似的机制在C++中是通过模板来实现的。典型的swap函数就可以通过宏机制来实现泛型的效果。

PRE00-EX5: 宏参数表现出的是“通过名称”调用机制,函数则是“通过值”调用。在必须实现“传名称”机制时,就得使用宏来实现

内联函数机制是在C99标准中引入的,C90标准可以使用静态函数。

内联替换既不是简单的文本替换也不会创建一个新函数,内联函数的优化过程是由编译器完成的,与程序员输入无关。

使用内联函数前需要考虑:

(1) 目标编译器支持情况;

(2) 使用后对系统性能的可能影响;

(3) 移植性需求

PRE01-C. 给宏参数名加上成对的小括号

PRE01-EX1: 宏参数被替换列表中包含逗号时不需要遵循本建议。实例如下:

 #define FOO(a, b, c) bar(a, b, c)
/* ... */
FOO(arg1, arg2, arg3);

PRE01-EX2: 使用宏的##和#机制使用符号拼接、字符串转换时不需要遵循本建议。实例如下:

#define JOIN(a, b) (a ## b)
#define SHOW(a) printf(#a " = %d\n", a)
PRE02-C. 宏的被替换参数列表需要用小括号包围 PRE02-EX1: 宏被展开为单个标识符或函数调用时可以不遵循此建议。实例如下:</p>
#define MY_PID getpid()

PRE02-EX2: 宏被展开为一个包含数组下表访问符[]、结构体或共用体成员访问符. 或 ->时可以不遵循此建议。实例如下:

#define NEXT_FREE block->next_free
#define CID customer_record.account.cid
#define TOOFAR array[MAX_ARRAY_SIZE]
PRE03-C. 优先使用typedef来定义包装类型 实例如下:</p>
typedef char * cstring;
cstring s1;
cstring s2;
PRE04-C. 不用复用一个标准头文件名 如果使用和标准头文件名相同的文件名,并且该自定义的头文件也在“头文件搜索路径”中,则其行为是“未定义”的。
PRE05-C. 正确理解拼接符号和字符串化时的宏行为
PRE06-C. 使用条件包含来防止头文件重复包含 实例如下:</p>
#ifndef HEADER_H
#define HEADER_H

/* ... contents of  ... */

#endif /* HEADER_H */
PRE07-C. 避免重复使用问号

根据C99标准定义,以下三个符号连在一起会被自动转换为特定的单个符号,如下:

??= # ??) ] ??! |
??( [ ??’ ^ ??> }
??/ \ ??< { ??- ~
PRE08-C. 确保头文件名的全局唯一性 PRE08-EX1: 一方面C99标准仅仅要求文件名的前8个字符是非常重要的; 另一方面,现代的操作系统和编译器都是支持长文件名的。 我个人认为最需要注意的是文件名大小写的问题,Windows系统上默认是不区分文件名大小写的,而*nix系统上则是区分文件名的大小写。因此,对于需要跨平台工作的代码,务必注意不要通过文件名大小写来区别不同文件。
PRE09-C. 不用使用废弃或过时的函数替换安全函数 这种情况通常发生在代码移植时,某个目标平台上缺少相应的安全版本函数时程序员为了省事,直接用宏定义用已有的不安全版本函数来“冒充”安全版本函数。
PRE10-C. 把多语句定义的宏包含在一个do-while循环中 正确实例如下:</p>
/*
 * Swaps two values.
 * Requires tmp variable to be defined.
 */
#define SWAP(x, y) \
  do { \
    tmp = x; \
    x = y; \
    y = tmp; } \
  while (0)
PRE11-C. 禁止在宏定义结尾处使用分号
PRE12-C. 禁止定义不安全的宏 在宏定义中使用表达式和操作符时需要小心,要避免“未定义”行为(宏中的操作符执行顺序、次数等都是“未定义”的)。危险实例代码如下:</p>
#define ABS(x) (((x) < 0) ? -(x) : (x))

void f(int n) {
  int m;
  m = ABS(++n); /* undefined behavior */
  /* ... */
}
PRE13-C. 不要在宏定义中改变控制流 该条建议被标记为“Work In Progress”
PRE30-C. 不要通过符号拼接的方式创建通用字符名 形如\unnnnnnnn或\unnnn的通用字符名(universal character name),根据C99标准中的定义:通过符号拼接方式(##)创建的通用字符名的行为是“未定义”的。本规则主要是提醒不要在代码中使用通用字符名作为变量名或标识符。
PRE31-C. 避免不安全的宏参数传递带来的副作用 PRE31-EX1: 确认无副作用时可以向不安全的宏传递参数表达式,但实际应用中并不建议这样做。(基本就是废话) 该条规则和PRE00-C. 优先使用内联或静态函数而不是函数式宏是相关的
PRE32-C. 禁止在宏参数中使用预处理指令
诸如#ifdef、#define、#include之类的预处理指令如果包含在宏参数列表中,其行为根据C语法标准是“未定义”的。
规则/建议条目全称 例外 笔记 /备注/点评
DCL00-C. 使用const修饰不变对象 DCL00-EX1: 可以定义无值的宏来防止头文件的重复包含问题。 除了使用const修饰符来“强制”定义一个对象为“不变”之外,也可以使用枚举常量或宏定义。
DCL01-C. 禁止在变量的子作用域范围内重用相同的变量名 DCL01-EX1: 函数声明和定义分开的声明时的参数名,只要定义时的参数名不和作用域范围内的变量名冲突即可。 避免变量名作用域范围的“二义性”,降低代码的可读性和可维护性
DCL02-C.使用一眼可见区别的标识符 类似0和o,l和1等等。特别注意那些在某些字体显示下几乎无差别的两个不同字符。
DCL03-C. 使用一个静态断言来验证常量表达式

C1X标准草稿C++ 0X标准草稿中新引入了一个静态断言函数

static_assert(constant-expression, string-literal);
DCL04-C. 禁止在一条声明语句中声明多个变量名 DCL04-EX1: for的多重循环语句中的多个控制变量声明是个例外。
提高代码的可读性和可维护性
DCL05-C. 使用typedef提高代码的可读性
DCL06-C. 使用有意义的符号来表示常量值 DCL06-EX1: 尽管使用有意义的符号来代替“无意义”的数字是有价值的,但在实际执行时要注意不能“过犹不及”。
常量值的表达方法 执行时间 占用内存 调试器可见 类型检查 编译时常量表达式
Enumerations 编译时 no yes yes yes
const-qualified 运行时 yes yes yes no
Macros 预处理 no no no yes
DCL07-C. 函数声明时包含合适的类型信息
  • 不要使用过时的函数声明语法
  • 避免使用“隐式”函数声明(函数定义即声明)
  • 函数指针和所指向的函数要类型匹配(返回值、参数个数、参数类型)
DCL08-C. 常量定义时注意合理的将常量之间的关系考虑进编码
DCL09-C. 将会返回errno的函数声明为返回值类型为errno_t 提高代码的可读性。实际使用注意errno_t类型是否有库文件的定义支持(一般定义在errno.h,虽然errno_t一般就是定义为int)。
DCL10-C. 确保可变参数列表的函数作者和调用者之间相互明确意图 小心处理可变参数,特别是参数列表的“结尾”条件判断
DCL11-C. 正确理解可变参数列表函数的参数类型处理 可变参数的参数类型是不会被编译器检查类型的,因此需要函数作者自己检查参数类型,并处理类型转换
DCL12-C. 使用黑盒类型(完全封装)实现抽象数据类型 C语言中实现抽象数据类型并实现“私有化”的一般方法是通过数据类型的声明和定义分开在两个不同的头文件中实现,在“公开”头文件中只引用数据并重新typedef,提供数据的访问方法,不提供数据定义语句。在“私有”头文件中定义数据类型。
DCL13-C. 将函数的指针参数常量值const化 这里的指针参数常量值泛指函数的参数是一个指针,且该指针所指向的值不会在函数执行过程中被改变
DCL15-C. 将不会被外部引用和链接的文件作用域范围对象或函数声明为静态 保持函数或对象“私有化”的一种方法,仅供本文件内代码引用和访问
DCL16-C. 使用’L’, 而不是 ‘l’, 来指示一个long类型值 提高代码可读性,避免将l看成数字1
DCL17-C. 小心被错误编译的volatile类型变量 按照C语言规范标准,声明为volatile的变量禁止编译器缓存优化,但在实际的编译器实现中,部分编译器的支持有bug,所以要小心使用这个修饰符。
DCL18-C. 十进制整数定义禁止使用打头 C99标准定义以0打头,紧跟(0-7)数字的整数为八进制
DCL19-C. 变量和函数的作用域范围最小化原则
DCL20-C. 即使函数不接受参数输入也要在函数声明时指定void参数 如果不指定void参数,而是使用空白参数列表,则编译器不会在编译时检查函数调用的参数列表。因此,调用该函数时使用任意参数都不会导致编译器产生错误,甚至不会产生任何编译警告。
DCL21-C. 理解复合文字的存储类型 C99标准中新定义了一种复合文字类型(compound literal): 用成对的小括号包围一个数据类型名,然后紧跟一个大括号对包围的初始化值列表。复合文字类型的值是大括号对包围的初始化值列表初始化的一个匿名对象。该匿名对象值的存储类型有可能是static(如果该复合文字变量是文件作用域范围),或者automatic(如果该复合文字变量是代码块作用域范围)
DCL30-C. 使用恰当的存储类型来声明对象 对象的生存储周期有三种:static、automatic和allocated,如果访问了超过生存周期的对象会导致“未定义”行为并且产生一个可被利用的漏洞。
DCL31-C. 先声明后使用标识符 C90标准允许隐式声明变量和函数,C99标准已经禁用了隐式声明。隐式声明可能会产生一些很难发现的bug
DCL32-C. 确保相互可见的标识符都唯一的 最典型的错误就是两个变量名的前31个字符是相同的,只有第32个字符不同,这在某些编译器看来就是两个相同的变量名。
DCL33-C. 确保restrict修饰的函数参数中的源和目的指针不指向交叠对象 所谓交叠对象指的是两个不同的对象存在部分共享的内存地址空间
DCL34-C. 使用volatile禁止数据缓存 这个规则的目的是禁止编译器级别的“过度”优化。例如异步信号处理过程中的变量修改可能是编译器不可见的,如果编译器“自作聪明”的缓存数据可能会导致“未期”的数据同步失败
DCL35-C. 函数调用时禁止使用和函数定义的参数类型不符的参数传入 通常违反该条规则会导致编译器产生编译期警告,程序员务必要解决这个警告。
DCL36-C. 禁止使用不同的链接类型声明相同的标识符 否则会产生“未定义”行为
DCL37-C. 不要使用预留实现的标识符

预留实现的标识符类型包括:

  • __打头或者_紧跟一个大写字母
  • _打头的标识符都预留用于文件作用域
  • 被包含头文件中所定义的每一个宏名的子块部分,除非在其他地方特别声明,否则应该避免使用
  • 所有的extern类型标识符
  • 被包含的头文件中有以上情况中任意一种子类

点评:C语言的变量、函数命名就是一个杯具,这时候就体现出C++名字空间的巨大意义了

DCL38-C. 声明变长数组成员时注意使用正确的语法

声明包含变长数组成员的结构体时有三点需要注意:

  • 不完全的数组类型成员必须是结构体中的最后一个元素
  • 结构体数组不能再包含变长数组成员
  • 包含变长数组成员的结构体不能作为其他结构体中的一个中间成员(必须是最后一个元素)
DCL39-C. 避免结构体内存对齐填充时的信息泄漏 不同编译器有自己的结构体对齐或禁止对齐指令或扩展函数,也可以在结构体初始化使用memset人为的将可能的填充位置先初始化为0,避免可能的信息泄漏问题

03. 表达式

规则/建议条目全称 例外 笔记 /备注/点评
EXP00-C. 运算符优先级问题使用括号解决
EXP01-C. 不要把指针的内存占用大小当做指针所指向数据的内存占用大小
EXP02-C. 小心逻辑“与”和“或”的短路行为
EXP03-C. 不要认为结构体的大小是结构体成员大小之和 注意字节对齐时的填充字节问题,很多编译器都提供一些标志位或控制指令来管理结构体的内存对齐和填充行为
EXP04-C. 禁止对结构体进行按字节比较 结构体的填充内容值和填充大小是“未确定”的
EXP05-C. 不要丢掉const修饰符 否则会引起编译器的“未定义”行为
EXP06-C. sizeof的运算对象不应该包含副作用 所谓副作用主要指的是sizeof的运算对象最好不要包含可能会引起自身值改变的运算符,sizeof运算对象中包含的表达式是否执行是“未确定”的。
EXP07-C. 在表达式中也要坚持使用常量而不是常量的值 常量的值也有可能会随着具体环境变化而变化,但我们只要坚持在表达式中使用的是常量名,而不是想当然的直接使用其值,就可以真正发挥常量的作用,享受常量定义带给我们的好处
EXP08-C. 确保指针算术运算的正确性 指针加法运算需要注意加数会被自动“缩放”乘以指针所指向的目标数据类型的单位长度
EXP09-C. 使用sizeof来确定一个数据类型或变量的大小 EXP09-EX1: C99标准明确声明sizeof(char)==1。因此任何基于字符或字符数组的数据类型大小计算可以不使用sizeof, 但这不适用于char *和任何其他数据类型。
EXP10-C. 代码的执行不要依赖子表达式的执行顺序

EXP10-EX1: &&和||可以保证表达式的执行顺序是 自左向右,第一个操作数执行后会产生一个顺序执行点。

EXP10-EX2: 条件表达式( ? : )的第一个操作数执行后会产生一个顺序执行点。第二个操作数当且仅当第一个操作数的执行结果不等于0; 第三个操作数当且仅当第一个操作数的执行结果等于0;

EXP10-EX3: 函数调用前存在一个顺序执行点,即函数名、实参、实参中的子表达式都会确定在函数调用前被执行。

EXP10-EX4:逗号运算符的左操作数总是先于右操作数执行,在左操作数执行完后会产生一个顺序执行点。

需要特别注意的是:函数参数列表中的逗号不是逗号运算符,只是用来分隔参数的。函数的多个参数的执行顺序可以是任意的,没有确定顺序!

这是C99标准里的一个经典“未确定”行为,执行结果是具体环境相关的
EXP11-C. 不要对不兼容的数据类型执行运算

C语言的数据类型转换是完全基于内存的,由于不同系统的内存组织和排列差异性等问题,可能会导致相同的类型转换语句在不同的系统上执行结果完全不相同。

bit结构存储的差异性是主要的罪魁祸首,不同的系统的bit存储顺序、是否允许跨存储单元边界有可能不相同。

EXP12-C. 不要忽视函数的返回值

EXP12-EX1: 如果函数的返回值无足轻重或者即使有错误也可以安全的被忽略的话。

EXP12-EX2: 如果一个函数永远不会执行失败或者返回值不存在错误值,返回值就可以忽略。例如strcpy函数。

EXP13-C. 把关系和相等运算符当做无结合性的

C语言中的关系运算符具有”左结合性”,即诸如a<b<c的语句,等效于(a<b)<c。如果a<b成立,则实际语句执行效果等效于1<c。

有时候可以利用这种结合性写一些比较geek的代码,但实际上这不利于代码的维护,也会降低代码的可读性。

EXP14-C. 在对小于int的整数进行bit按位运算时要小心整数提升规则 对小于int的整数类型执行运算时会被自动“类型提升”。如果原始数据都可以用int表示,则原始的较低类型数据会被自动转换为int;否则,会被自动提升为unsigned int。如果是转换为更大的数据类型,则原始的无符号整数会被填0扩充,原始的有符号整数会被带符号扩充。因此,对小于int的整数类型执行按bit运算可能会产生意外的结果。
EXP15-C. 禁止在if, for, while语句的同一行上写分号
EXP16-C. 禁止将函数指针和常量进行比较 除非是和相同类型的NULL指针常量比较
EXP17-C. 禁止在条件语句中使用按位运算符 通常都是“笔误”的象征,一般我们在条件语句中都是使用逻辑与和或操作,而不是按位与、或操作
EXP18-C. 禁止在选择语句中使用赋值操作 原因同EXP17-C
EXP19-C. 坚持给if, for, while语句使用大括号
EXP20-C. 执行“显式”的测试来确定成功、true or false,相等

推荐使用:

if (foo() != 0) ...

而不是:

if (foo()) ...
EXP21-C. 把常量放在相等比较操作符的左侧 如果把==误写为=,编译器在编译时就会给出错误警告。
EXP30-C. 禁止依赖代码执行顺序点之间的执行顺序
EXP31-C. 避免断言的副作用 通常断言是否生效依赖于代码在编译时是否定义了NDEBUG宏
EXP32-C. 禁止通过一个non-volatile引用访问一个volatile对象 否则是一个“未定义”行为
EXP33-C. 禁止引用一个未初始化的内存 否则是一个“未定义”行为
EXP34-C. 禁止对空指针解引用 否则是一个“未定义”行为
EXP35-C. 禁止访问或修改一个经过一系列顺序执行点之后的函数返回值中的数组 否则是一个“未定义”行为,C语言的函数也许不会直接返回一个数组,但可能返回一个包含数组的共用体或者结构体。即不要在同一条语句中试图同时先调用函数再访问或修改这个函数的数组返回值
EXP36-C.禁止将指针转换为一个更严格对齐的指针类型 否则是一个“未定义”行为
EXP37-C. 按照API的设计意图去调用函数
EXP38-C. 禁止对bit域成员或非法类型调用offsetof()

offsetof()宏提供了一种可移植的计算结构体成员在结构体中的相对内存偏移量的方法,该宏的执行结果是一个常量表达式,值的类型为size_t,表示目标结构体成员在目标结构体中的相对内存偏移量是多少bytes。

如果使用offsetof()计算结构体中的bit域成员或者是非法成员名,其行为是“未定义”的。

bit域成员实例如下:

struct S {
  unsigned int a: 8;
} bits = {255};

int main(void) {
  size_t offset = offsetof(struct S, a);  /* error */
  printf("offsetof(bits, a) = %d.\n", offset );
  return 0;
}

上面的例子中,只需要将

  unsigned int a: 8;

改为:

  unsigned int a;

即可。

EXP39-C. 禁止通过一个类型不兼容的指针访问变量
EXP40-C. 禁止修改常量值
EXP41-C. 禁止对一个指针加或减一个乘过“系数”的整数 指针的算术运算仅在指针指向一个数组类型才有意义,并且当执行指针的算术运算时,加数和减数都会自动“乘以”一个系数(指针所指向的数组对象的单个元素的大小)