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

Posted by c4pr1c3 on February 5, 2011

04. 整数

</tbody> </table>

05. 浮点数

规则/建议条目全称 例外 笔记 /备注/点评
INT00-C. 理解你的代码实现中的数据模型
数据模型定义了标准数据类型的存储空间大小。如char在几乎所有的平台下的实现都是8字节长度,long在IA-32/64都是32字节,但在64-bit Linux/*BSD下是64字节。因此,当程序需要跨多个应用平台运行时需要考虑数据模型的实现差异可能对程序运行中的一些“假设”前提条件的影响。
INT01-C. 使用rsize_t或者size_t来表示一个对象所占用空间的整数值单位
size_t的最大值由SIZE_MAX宏定义,一般情况下size_t能够覆盖所有可用的地址空间数值表示范围。ISO/IEC TR 24731-1-2007标准中引入了一个新的数据类型rsize_t,定义为size_t但显式的用于表示一个单个对象的大小。支持rsize_t的代码,可以使用RSIZE_MAX来判断一个单个对象的大小不超过RSIZE_MAX(一个正常单个对象的内存占用的最大值),库函数可以使用rsize_t来进行输入校验。
INT02-C. 理解整数转换规则
整数的转换可能发生在显式的加法运算过程中或者是某些运算符的要求,对于整数的转换规则来说特别需要注意有符号整数和无符号整数之间的转换规则,除此之外就是和整数的“转换等级”相邻的数据类型和整数之间的相互转换规则。
INT03-C. 使用一个安全的整数库

对抗整数类漏洞的第一道防线是数据有效范围检测,特别是当输入参数个数较多、进行大数运算时,整数类漏洞是最容易被触发的。具体来说,以下是最常见的整数类漏洞发生场景:
1. 数组的下标
2. 指针运算
3. 对象的长度或大小
4. 数组的边界值(例如,循环中的计数器)
5. 内存分配函数的参数
6. 关键安全代码
CERT/CC为Windows平台提供了一个IntegerLib。

INT04-C. 强制限来自不安全(输入)源的整数值大小
INT05-C. 如果无法处理所有可能的输入值时禁止使用输入函数来转换字符数据 例如格式化输入函数,如scanf(), fscanf(), vscanf(), vfscanf()被用于读取来自标准输入或者(仅限于fscanf(), vfscanf())其他输入流的字符数据时是安全的。这些函数对合法的整数值范围输入是可以正常处理的但非法值的处理能力不够强健。
以Linux平台的scanf()函数为例,如果对于输入的整数数据的“格式化”转换结果无法使用提供的格式化字符串来存储时,scanf()会设置错误变量errno为ERANGE。需要注意的是,检查errno是否等于ERANGE的方法在Linux平台上是可行的,但并不保证在其他平台上也一定可行。只有在不需要考虑平台移植性的前提条件下,才可以使用这个方法。
INT06-C. 使用strtol()或者其他相关函数来转换一个字符标记串为一个整数 strtol()系列函数被设计用来替换如下不安全的“等效”函数</p>
atoi: (int)strtol(nptr, (char **)NULL, 10)
atol: strtol(nptr, (char **)NULL, 10)
atoll: strtoll(nptr, (char **)NULL, 10)
INT07-C. 使用显式的有符号或无符号字符类型来表示数值 INT07-EX1: 规则FIO34-C. Use int to capture the return value of character IO functions中提到某些字符IO函数的返回值是int。尽管返回值是算术类型,但本质上返回值并不是数值类型,因此可以使用char类型来存储这一类函数的返回值。
Note: char, signed char, unsigned char被统称为字符类型。但char和其他两种字符类型是不相互兼容的,因此在需要使用字符类型表示数值时必须要从signed char和unsigned char中二选一。</p>
INT08-C. 验证所有整数值都在整数值的有效范围之内
最典型的一个例子就是MAX+1,如果加法的两个操作数都是unsigned int,且返回结果也是unsigned int来存储,则根据C标准,其结果将是MIN。而这在绝大多数的应用场景显然是一种错误的“逻辑”,很多情况下,MAX+1我们期待返回的值是MAX,而不是MIN。
INT09-C. 确保枚举类型常量被映射为唯一值 INT09-EX1:类似以下代码:</p>
enum Color { red, orange, 
  yellow, green, blue, indigo,
  violet=indigo };

虽然存在相同的常量值映射,但由于采用常量依赖关系(相对偏移量)的方法定义的常量,只要不用在需要枚举常量值唯一性(switch case)的场景中是完全可以的,无论是人工代码检查还是自动化的代码检查工具都可以正确的理解和发现可能的隐藏逻辑bug。</td>

枚举类型在初始化时允许默认赋值和自定义赋值两种方式,默认赋值方式可以保证映射值的唯一性(递增规律),一般出问题就出在自定义赋值过程中。如下就是一个错误赋值的例子:</p>
enum Color { red=4, 
  orange, yellow, 
  green, blue, indigo=6, 
  violet };

上面的代码本身没有语法错误,但如果用在switch语句中的case分支逻辑,就有可能导致“不显眼”的“短路”代码分支。 </td> </tr>

INT10-C. 在使用%运算符时不要假设余数一定为正
INT11-C. 整数和指针相互转换时要小心 整数和指针间的转换仅在常量0时是没有副作用的。除此之外的相互转换是编译器实现相关的。
INT12-C. 在表达式中使用的普通bit位整数值不要轻易认定其数据类型 以下为典型的“未定义”行为代码实例:</p>
struct {
  int a: 8;
} bits = {255};

int main(void) {
  printf("bits.a = %d.\n", bits.a);
  return 0;
}

上面的代码打印-1或者255都是有可能的,原因在于编译器在编译上述代码时如何表示bits.a。上面的代码消除二意性的方法就是在定义结构体bits的时候,显式的指定结构体成员a为unsigned int或者signed int,这样就能确定性的保证bits.a的值为255或者-1。

INT13-C. 仅对无符号操作数执行按位运算符 INT13-EX1: 如果是对预编译宏作为&和|操作符的参数时,即使预编译宏的值没有被声明为unsigned,仍然可以使用&和|操作符。
代码实例如下:</p>
fd = open(file_name, UO_WRONLY |
  UO_CREAT | UO_EXCL | UO_TRUNC,
  0600);

INT13-EX2: 如果是编译期可以确定的正数,则在作为移位运算符的右操作数时即使是一个signed类型值也是可以的。实例代码如下:

#define SHIFT 24
foo = 15u >> SHIFT;
INT14-C. 避免对同一个数据执行按位操作和算术运算
否则会降低代码的可读性。
INT15-C. 在程序员自定义的整数类型应用于格式化IO时使用intmax_t或者uintmax_t 这两种数据类型是C99标准中新引入的,这两种数据类型可以表示任何相同符号类型的其他整数类型的任何值。在格式化IO的参数类型修饰符中,长度修饰符j后紧跟的d, i, o, u, x, X 或者n转换指示符可以应用于参数列表中的intmax_t或者uintmax_t类型参数。</p>
INT16-C. 不要对有符号整数的表达方式作任何假设 C99标准中规定“有符号整数的bit表达方式是具体实现相关的”,并允许以下三种有符号整数的bit位表示方法:
1. 符号和数量
2. 2的补数
3. 1的补数
INT17-C. 将整数常量定义为具体编译器实现无关的形式 整数常量通常被用做掩码或者特定的bit值,一般我们习惯直接使用十六进制表示法来定义这个值。但在跨平台(主要是不同CPU架构,如32位和64位平台)时直接使用十六进制表示可能会有问题。如下代码实例:
首先是具体编译器相关的代码形式:</p>
const unsigned long mask = 0xFFFFFFFF;  
unsigned long x;

/* Initialize x */

x = (x ^ mask) + 1;

然后是改进后的编译器实现无关的“等价”代码:

const unsigned long mask = ~0;
unsigned long x;

/* Initialize x */

x = (x ^ mask) + 1;
INT30-C. 确保无符号整数操作符不会造成数据“溢出”变小 INT30-EX1: 无符号整数的运算可能表现出类似“整数求模”运算的性质特点,如果确实有需求要实现成这种“求模”运算的性质,也必须要在相关代码的注释中明确说明,以提高代码的可维护性。
INT32-EX2: 如果能在编译器就确定无符号整数的操作结果一定不会出现“溢出”变小的可能,则无须在运行时检查这种特殊的“溢出”行为。</p>
INT31-C. 确保整数转换的结果不会引起数据丢失或者误表达 INT31-EX1: C99标准定义了标准整数类型的最小取值范围,但实际的平台实现和支持可能支持大于这个取值范围的整数类型,因此在特定平台上也许不需要进行整数转换结果的精度检查。
INT32-C. 确保有符号整数的运算不会导致溢出 有符号整数的溢出行为是“未定义”的。
INT33-C. 确保除法和取模运算符不会出现“除0”错误 否则其行为是“未定义”的。
INT34-C. 不要在移位操作时移位“负数”个bit或者超过实际可移动的bit数 否则其行为是“未定义”的。
INT35-C. 在比较或赋值整数表达式结果前先用更大数值类型执行这个表达式
规则/建议条目全称 例外 笔记 /备注/点评
FLP00-C. 理解浮点数的局限性 浮点数的精度是有限的,无论其底层实现机制如何,在涉及近似取舍精度时都容易出现错误。因此在程序中涉及浮点数计算中的精度问题不要轻易假设精度位数。
FLP01-C. 调整浮点数运算表达式计算顺序时要小心 以下为代码实例:</p>
double x, y, z;
/* ... */
x = (x * y) * z; /* not equivalent to x *= y * z; */
z = (x - y) + y ; /* not equivalent to z = x; */
z = x + x * y; /* not equivalent to z = x * (1.0 + y); */
y = x / 5.0; /* not equivalent to y = x * 0.2; */
  
FLP02-C. 避免在需要精确计算时使用浮点数 注意误差的累计和扩散问题,必要的时候可以使用二进制位运算或整数运算来“模拟”浮点数运算过程以保证计算的精度符合要求。
FLP03-C. 检测并处理浮点数误差和异常
FLP04-C. 检查浮点数输入中的异常值 即使NaN和infinity允许作为程序输入,也要确保在计算过程中不要将这两个值参与数值计算过程以避免程序异常出错。 最典型的一个例子就是NaN==NaN返回false!使用isinf和isnan宏来判断输入数据是否是inf或者NaN。
FLP05-C. 禁止使用非正规化数字(denormalized numbers) 绝大多数C语言编译器实现都是使用IEEE 754标准来表示浮点数。float的编码规则是:1个符号位、8个指数位、23个尾数位,double的编码规则是:1个符号位(s)、11个指数位(E)、52个尾数位(M)。浮点数的计算方法就是:(-1)s * M * 2E
FLP30-C. 禁止使用浮点数作为循环计数器
FLP31-C. 禁止向只接受实数输入的函数传递复数 否则是“未定义”行为。
FLP32-C. 预防或检测数学函数中的自变量域和值域错误
FLP33-C. 在浮点数运算中将整数转换为浮点数 FLP33-EX1: 如果确有需要在整数转换为浮点数之前进行计算,务必在代码注释中有所说明。 在整数和浮点数相互转换之后,务必要进行取值范围检查,以避免“未定义”行为产生。
FLP34-C. 确保浮点数转换后的结果在新数据类型的允许数值范围之内 如果转换后的数值超过新数据类型的允许数值范围之内,其结果是“未定义”的。
FLP35-C. 浮点数比较大小时需要考虑精度问题
FLP36-C.整数转换为浮点数时需要考虑精度损失 原因同FLP34-C.
FLP37-C. 函数的浮点数类型返回值要截短

06. 数组

规则/建议条目全称 例外 笔记 /备注/点评
ARR00-C. 理解数组的原理 数组的下标访问操作符[]和下标的重要性,要确保数组访问不越界,不被任意控制;
注意数组元素在内存排布上的连续性;
数组适合需要随机读访问的场景,但不适合需要随机插入元素的场景。
ARR01-C. 当需要获知数组大小时禁止使用sizeof运算符于指针 数组名可以看作是一个指针,但不等同于指针。函数的形参声明为一个数组时,在函数定义中使用sizeof()作用于形参,等效于应用于一个指针。只有当sizeof应用于数组名(而不是指针)时才可以获知数组的总内存占用大小。
以下为错误代码实例:</p>
void clear(int array[]) {
  for (size_t i = 0; 
   /* sizeof(array) == 4 on IA32 */ 
   /* sizeof(array[0]) == 4 on IA32 */
    i < sizeof(array) / sizeof(array[0]); 
    ++i) { 
     array[i] = 0;
   }
}

void dowork(void) {
  int dis[12];

  clear(dis);
  /* ... */
}
  

以下为正确代码实例:

void clear(int array[], size_t len) {
    for (size_t i = 0; i < len; i++) {
     array[i] = 0;
  }
}

void dowork(void) {
  int dis[12];

  /* sizeof(dis) / sizeof(dis[0]) == 12 */
  clear(dis, sizeof(dis) / sizeof(dis[0]));
  /* ... */
}
  
ARR02-C. 即使使用初始化表达式隐式声明了数组的容量也应该明确指定数组的边界 提高代码的健壮性,避免“隐藏”bug。
ARR30-C. 禁止构造或使用越界指针或数组下标 否则会造成“未定义”行为。
ARR31-C. 在不同的源代码文件中使用统一的数组表示方法 数组名在作为函数形参时会被自动转换为指针,除此之外,在一个源文件中使用指针形式声明一个数组而在另一个文件中使用数组形式声明则不是等价的。
ARR32-C. 确保变长数组的容量参数取值合法
ARR33-C. 确保拷贝目标有足够的空间 否则会造成缓冲区溢出。
ARR34-C. 确保表达式中的数组类型是相互兼容的 否则是“未定义”行为。
ARR36-C. 禁止对指向两个不同数组的指针进行相减或比较操作
ARR37-C. 禁止对指向非数组的指针加或减一个整数

07. 字符(数组)和字符串(STR)

</tbody> </table>

规则/建议条目全称 例外 笔记 /备注/点评
STR00-C. 使用合适的数据类型表示字符 C语言支持单字节、多字节和宽字节字符,单字节和多字节字符串都是用null结尾同时被包含null字符在内的字节串表示,也被称为“窄字符串”。宽字节字符串是以null结尾并且包含null宽字符在内的宽字符(wchar_t)串。由于C语言的字符串底层实现是以数组为基础的,所以字符串也存在和数组类似的安全问题。C语言支持的字符串类型包括:</p>
    signed char / unsigned char:适合于小整数值
    “plain” char: 适合于小字符集字符串表示
    int: 注意使用中可能存在的二义性,如isspace(‘\200’)当参数是signed char时其结果是“未定义”的。
    unsigned char: 字符串比较函数的内部使用数据结构类型,适合于任意数据类型,特别是二进制字符串的操作。
    wchar_t: 宽字符用于多国语言的字符数据表示
STR01-C. 设计并实现一致的字符串表示方案 字符串表示方案可以分为静态分配的数组和动态分配内存两种方案,两种方案各有千秋。静态分配的数组在字符串长度超过数组可用长度后会造成输入数据的截断,动态分配内存的字符串如果不严格检查输入数据,可能会导致内存耗尽的DoS攻击。一般在一些可用性要求很高的系统中,要尽可能避免使用动态分配内存方案。本条建议最重要的要求是:在同一个项目代码中,统一使用一种字符串表示方案,而不是让不同的程序员随心所欲的使用两种方案。
STR02-C. 在将数据传递给复杂子系统之前做好数据清洗 典型的复杂子系统包括:</p>
    通过system()调用传递给命令解释器
    外部程序
    关系型数据库
    第三方COTS组件(如企业资源计划子系统)
STR03-C. 禁止不可逆的截断一个null结尾字符串 STR03-EX1: 除非程序员是有意要截断字符串长度 C语言编程规范里规定了一组限制长度的字符串操作函数来代替那些不检查长度的“不安全”函数,如下:</p>
strncpy() instead of strcpy()
strncat() instead of strcat()
fgets() instead of gets()
snprintf() instead of sprintf()
  

这些函数在字符串超过限定长度时会主动截断字符串以适应给定的缓冲区长度,但需要注意的是,类似strncpy()是不保证截断后的字符串也是以null结尾的!这就可能导致一些“隐藏”的软件漏洞。 </td> </tr>

STR04-C. 对于小字符集的字符串表示使用普通char char类型是和signed char/unsigned char不兼容的char数据类型,普通char在这里指的就是char。绝大多数情况下,char类型数据的可移植操作符只有赋值和等号操作符(=, ==, !=),除此之外,如果是从数字转换过来或转换为数字的char,则可以使用其他数字相关的操作符。如:</p>
 
  char c = '0'; /* c是数字0 */
  
STR05-C. 引用字符串文本时使用指向常量的指针 如果不打算改变字符串的内容就使用const修饰符,否则可以使用字符串数组。例如如下代码:</p>
char c[] = "Hello";
c[0] = 'C'; // valid
char *d = "Hello";
d[0] = 'C'; //undefined behavior
  
STR06-C. 不要以为strtok()函数不会改变被解析的字符串 每次strtok()找到一个匹配的token,都会将目标字符串中相应位置的token置为”,所以使用strtok()函数时建议将要解析的字符串“拷贝”一次,对“拷贝”字符串调用strtok()函数。
STR07-C. 使用TR 24731建议中的安全函数来操作字符串 ISO/IEC TR 24731-1标准是由微软建议并开发出的一套用以替换strcpy(), strcat(), strncpy(), 和strncat()函数的*_s()系列函数。
STR08-C. 使用受管理的字符串来开发新字符串操作代码 使用一个公开标准的字符串操作API,使用该API可以避免由程序员负责字符串存储空间的分配和重新分配,程序员只需要负责在不使用字符串时释放内存。
STR09-C. 在表达式中使用char时不要假设数字值 STR09-EX1: 在支持ASCII字符集或Unicode字符集的平台上,类似a~z字符在数值是一定连续的。本建议只是从可移植性的角度出发,如果目标平台不支持ascii字符集,类似a~z这样的连续字符其数值可能是非连续的。
STR10-C. 禁止拼接不同类型的字符串 拼接宽字符和窄字符会导致“未定义”行为。
STR30-C. 不要尝试去修改字符串文本 字符串文本指的是用双引号包含的0或多个多字节字符,宽字符串文本和字符串文本一样,区别仅仅在于多了一个前缀L。违反本规则会导致“未定义”行为。
STR31-C. 确保字符串有足够的空间存储字符数据和null字符
STR32-C. 必要时保证字节字符串以null结尾
STR33-C. 正确计算宽字符串的长度 使用wcs*系列字符串操作函数而不是str*系列函数
STR34-C. 在将char类型字符串转换为更大的整数类型时先执行类型转换为unsigned char
STR35-C. 禁止从无长度限制的输入拷贝数据到一个定长数组
STR36-C. 禁止在使用一个字符串文本初始化字符串数组时指定数组的大小 以下代码是错误实例:</p>
const char s[3] = "abc";
  
STR37-C. 字符串处理函数的参数必须表示为unsigned char类型
STR38-C. 禁止宽字符串和窄字符串操作函数相互替代使用