术语定义
- 规则:编程时必须遵守的约定。
- 建议:编程时必须加以考虑的约定。
- 说明:对此规则/建议进行必要的解释。
- 错误示例:对此规则/建议从反面给出例子。
- 推荐做法:对此规则/建议从正面给出例子。
通用规则
规则1:对外部输入进行校验
说明:软件最为普遍的缺陷就是对来自客户端或者外部环境的数据没有进行正确的合法性校验。这种缺陷可以导致几乎所有的程序弱点,例如Dos、内存越界、命令注入、SQL注入、缓冲区溢出、数据破坏、文件系统攻击等。这些不可信数据可能来自:
- 用户输入
- 外部调用的参数
- 进程间的通信数据
- 网络连接(甚至是一个安全的连接)、
- 用户态输入(对于内核程序)
- 上层应用(业务)输入
当这些不可信输入用于如下场景时(包括但不局限于),需要校验其合法性:
- 作为循环条件 – 可能会引发缓冲区溢出、内存越界读/写、死循环等问题。
- 作为数组下标 – 可能导致超出数组上限,从而造成非法内存访问。
- 作为内存偏移地址 – 指针偏移访问内存,可能造成非法内存访问,并可以造成进一步的危害,如任意地址读/写。
- 作为内存分配的尺寸参数 – 请参考规则
C3.1
、C3.2
和C4.5
。- 作为业务数据 – 如作为命令执行参数、拼装sql语句、拼接格式化字符串等,这会导致命令注入、SQL注入、格式化漏洞等问题。详细请参考规则
C2.1
、C5.2
和C6.3
。- 用于数据拷贝操作 – 当作为拷贝长度时,极易造成目标缓冲区溢出。详细请参考规则
C1.1
、C1.2
和C1.3
。- 影响代码逻辑 – 比如基于不可信输入做安全决策,影响代码逻辑走向。
- 会改变系统状 – 比如未加校验直接打开不可信路径,可能会导致目录遍历攻击,操作了攻击者无权操作的文件,使得系统被攻击者所控制。
输入校验可能包括如下内容(包括但不局限于):
- 校验数据长度
- 校验数据范围
- 校验数据类型和格式
- 校验输入只包含可接受的字符(可以采用『白名单』形式),尤其需要注意一些特殊情况下的特殊字符。了解更多关于特殊字符,可以参考附录A和附录B。
规则2:禁止在日志中保存口令、密钥
说明:在日志中不能保存口令和密钥,其中的口令包括明文口令和密文口令。对于敏感信息建议采取以下方法,
- 不打印在日志中;
- 若因为特殊原因必须要打印日志,则用
*
代替(不要显示出敏感信息的长度)。
规则3:及时清除存储在可复用资源中的敏感信息
说明
:存储在可复用资源中的敏感信息如果没有正确的清除则很有可能被低权限用户或者攻击者所获取和利用。因此敏感信息在可复用资源中保存应该遵循存储时间最短原则。可复用资源包括以下几个方面:
- 堆(heap)
- 栈(stack)
- 数据段(data segment)
- 数据库的映射缓存
存储口令、密钥的变量(包括加密后的变量)使用完后必须显式覆盖或清空
。
规则4:正确使用经过验证的安全的标准加密算法
说明:禁用私有算法或者弱加密算法(如DES,SHA1等),应该使用经过验证的、安全的、公开的加密算法。加密算法分为对称加密算法和非对称加密算法。推荐使用的
- 常用对称加密算法有:
- AES
- 推荐使用的常用非对称算法有:
- RSA
- 推荐使用的数字签名算法有:
- 数字签名算法(DSA)
- ECDSA
- 此外还有验证消息完整性的安全哈希算法(SHA256)等。基于哈希算法的口令安全存储必须加入盐值(参见
规则5
)。密钥长度符合最低安全要求:
- AES: 128位
- RSA: 2048位
- DSA: 2048位
规则5:基于哈希算法的口令安全存储必须加入盐值(salt)
说明:单向哈希是在一个方向上工作的哈希函数,从预映射的值很容易计算其哈希值,但要根据特定哈希值产生一个预映射的值却是非常困难的。单向哈希主要应用于加密、消息完整性校验、冗余校验等。
假如没有加入盐值,则加密原理是:密文=哈希算法(明文)
此时,若攻击者获取到密文,同时知道哈希算法,则就可以通过字典攻击来探测和获取口令。
加入盐值之后:密文= 哈希算法(明文+盐值)
其中盐值可以随机设置,这样即使相同的口令,但盐值不同,密文也不同,从而增加了口令的破解难度、增强安全性。
规则6:不要硬编码敏感信息
说明:
- 硬编码口令、服务器IP地址以及加密密钥等敏感信息可能会将这些信息暴露给攻击者。任何人都可以反编译并发现这些敏感信息。因此,除了一些特殊情况(例如在TPM环境下)之外,程序中禁止硬编码任何敏感信息。
- 硬编码敏感信息还会增加维护管理成本,当修改代码时,需要额外管理并适配这些修改。例如,要更改一个已经部署了的程序的硬编码口令,可能需要下发一个补丁。
规则7:不要在共享目录中创建临时文件
说明:程序员经常会在共享目录里创建临时文件。临时文件通常作为不需要或者不能驻留在内存中的数据的一种辅助存储方式,同时也可以作为与其它进程通过文件系统进行通信的一种方式。例如,一个进程会以一个公认的命名或者与合作进程协商好的名字在共享目录里创建临时文件,然后这些临时文件便可以在这些合作进程间共享信息。
但是,这是一个非常危险的操作。一个在共享目录里大家都知道名字的文件是很容被攻击者控制和操纵的。以下列出了几种可能的规避方法:
- 使用其它低级别进程间通信(IPC)机制,如使用sockes或者共享内存;
- 使用高级别IPC机制,如远程过程调用(remote procedure call);
- 使用一个安全的目录或者设置一个只能被程序应用实例访问的jail(确保同一平台下的多个应用程序实例不会产生竞争)。
IPC机制中有些需要使用临时文件,但是其它的不需要。例如,需要使用临时文件的IPC机制有POSIX的mmap()函数。而伯克利套接字(Berkeley Sockets)、POSIX本地IPC套接字和System V共享内存却不需要临时文件。因为共享目录的多用户属性使得它具有与生俱来的危险,因此,利用共享临时文件来实现IPC是不推荐的。
当2个以上或者一组用户对目录具有写权限时,其危险和欺骗性比少量文件的共享访问更为严重。因此,当确实需要在共享目录中创建临时文件时,必须满足如下条件:
- 创建不可预测的文件名称;
- 创建唯一的文件名称;
- 原子打开;
- 独占打开;
- 使用合适的权限打开;
- 程序退出前必须删除。
规则8:遵循最小权限原则
说明:程序在运行时可能需要不同的权限,但对于某一种权限不需要始终保留。例如,一个网络程序可能需要超级用户权限来捕获原始网络数据包,但是在执行数据报分析等其它任务时,则可能不需要相同的权限。因此程序在运行时只分配能完成其任务的最小权限。过高的权限可能会被攻击者利用并进行进一步的攻击。因此,权限在使用完毕后应该及时撤销。在撤销权限时,应该尤其注意以下两点:
- 撤销权限时应遵循正确的撤销顺序;
- 完成权限撤销操作后,应确保权限撤销成功。
C安全编程
字符串操作安全
规则C1.1:确保有足够的空间存储字符串的字符数据和’\0’结束符
说明:在分配内存或者在执行字符串复制操作时,除了要保证足够的空间可以容纳字符数据,还要预留’\0’结束符的空间,否则会造成缓冲区溢出。
错误示例1:拷贝字符串时,源字符串长度可能大于目标数组空间。
enum { BUFFERSIZE = 128 };
int main(int argc, char *argv[])
{
char dst[BUFFER_SIZE] = {
0x00};
if (argc > 1)
{
strcpy(dst, argv[1]); /*【错误】当源字符串长度大于目标数组dst时,会发生缓冲区溢出 */
}
return 0;
}
推荐做法1:根据源字符串长度来为目标字符串分配空间。
int main(int argc, char *argv[])
{
char *dst = NULL;
size_t length = strnlen(argv[1], MAX_LEN);
if (argc > 1)
{
dst = (char *)malloc(length + 1); /* 【修改】 确保目标字符串可以存储源数据 */
if(dst != NULL)
{
int ret = strcpy_s(dst, length + 1, argv[1]); /* 【修改】使用安全函数strcpy_s代替strcpy来拷贝字符串 */
/* 校验ret,确保安全函数执行成功 */
}
}
/* 使用完后释放内存 */
return 0;
}
错误示例2:典型的差一错误。for循环将src中的数据拷贝到dst中,然而未考虑’\0’结束符写入数组的位置,经过循环后,’\0’会写越界,超出数组dst一个字节,从而造成缓冲区溢出和内存改写。
enum { ARRAY_SIZE = 32 };
void func(void)
{
char dst[ARRAY_SIZE + 1] = {
0x00};
char src[ARRAY_SIZE + 1] = {
0x00};
size_t i = 0;
for (i = 0; src[i]!= ‘\0’ && (i < (ARRAY_SIZE + 1)); ++i) /* 【错误】 结束符会写越界dst一个字节*/
{
dst[i] = src[i];
}
dst[i] = '\0'; /* 【错误】 结束符写越界*/
}
推荐做法2:在赋值循环语句中考虑结束符的添加。
enum { ARRAY_SIZE = 32 };
void func(void)
{
char dst[ARRAY_SIZE + 1] = {
0x00};
char src[ARRAY_SIZE + 1] = {
0x00};
size_t i = 0;
for (i = 0; src[i] != ‘\0’ && (i < ARRAY_SIZE); ++i) /* 【修改】 考虑结束符的写入位置,不会越界 */
{
dest[i] = src[i];
}
dst[i] = '\0';
}
错误示例3:在下面的例子中,name是可能来自用户输入、文件系统或者网络的字符串变量。代码中通过构造的一个文件名称来打开文件。然而,由于sprintf()函数没有对输入的数据进行校验,因此当name是一个非常大的字符串变量时,就会产生缓冲区溢出。
void func(const char *name)
{
char filename[NAME_SIZE + 1] = {
0x00};
sprintf(filename, "%s.txt", name); /* 【错误】 当name的长度超过目标数组filename大小时,会发生缓冲区溢出 */
}
推荐做法3:一个比较好的方法就是使用安全版本函数sprintf_s()。sprintf_s()会对入参进行校验,保证不会发生缓冲区溢出。
void func(const char *name)
{
char filename[NAME_SIZE + 1] = {
0x00};
int ret = sprintf_s(filename, sizeof(filename), "%s.txt", name); /* 【修改】使用sprintf_s来避免缓冲区溢出 */
/* 校验ret,确保安全函数执行成功 */
}
规则C1.2:字符串操作过程中确保字符串有’\0’结束符
说明:字符串结束与否是以’\0’作为标志的。没有正确地使用’\0’结束字符串可能导致字符串操作时发生缓冲区溢出。因此对于字符串或字符数组的定义、设置、复制等操作,要给’\0’预留空间,并保证字符串有’\0’结束符。
错误示例:下列代码中,在调用strncpy()之前ntca就被赋予了结束符。然而,接下来执行的strncpy()可能会将这个结束符覆写。
char ntca[NTCA_SIZE + 1];
ntca[sizeof(ntca) - 1] = '\0';
strncpy(ntca, source, sizeof(ntca)); /*【错误】 strncpy()不能保证字符串结尾含有结束符,因此可能将已经赋予的结束符覆写 */
推荐做法:正确的方法是依照程序员的目的而来的。如果程序员想要截断一个字符串并且保证目标字符串结尾含有结束符,那么可以使用如下方法。
char ntca[NTCA_SIZE + 1];
int ret = strncpy_s(ntca, NTCA_SIZE + 1, source, NTCA_SIZE); /*【修改】 使用strncpy_s()来代替strncpy(),可以保证结尾含有结束符 */
/* 校验ret,确保安全函数执行成功 */
规则C1.3:把数据复制到固定长度的内存前必须检查边界
说明:将未知长度的数据复制到固定长度的内存空间可能会造成缓冲区溢出,因此在进行复制之前应首先获取并检查数据长度,并且在任何情况下都要在明确目标缓冲区的长度之后再进行复制操作。
错误示例:输入消息长度不可预测,不加检查的复制会造成缓冲区溢出。
void MsgCopy()
{
char dst[MAX_SIZE + 1] = {
0x00};
char *temp = getInputMsg();
if(temp != NULL)
{
strcpy(dst,temp); /*【错误】当temp长度大于dst大小时,会产生缓冲区溢出 */
}
}
推荐做法:
void MsgCopy()
{
char dst[MAX_SIZE + 1] = {
0x00};
char *temp = getInputMsg();
if(NULL == temp)
return;
size_t len = strlen(temp);
if(len > MAX_SIZE) /* 【修改】 校验temp的长度是合法的 */
{
return;
}
int ret = strcpy_s(dst, sizeof(dst), temp); /* 【修改】使用安全版本函数strcpy_s()来代替strcpy()来进行操作 */
/* 校验ret,确保安全函数执行成功 */
}
规则C1.4:避免字符串/内存操作函数的源指针和目标指针指向内存重叠区
说明:内存重叠区是指一段确定大小及地址的内存区,该内存区被多个地址指针指向或引用,这些指针介于首地址和尾地址之间。使用函数拷贝内存重叠的对象可能导致未定义的行为,可被用来破坏数据的完整性。
错误示例1:sprintf_s()函数不当使用
void LogMessageItem(int error_type , char * error_msg)
{
size_t msg_length = strnlen(error_msg, MAX_LEN);
int ret = sprintf_s(error_msg, msg_length, "%d:%s",error_type,error_msg); /* 【错误】 err_msg变量既是源缓冲区,又是目标缓冲区 */
/* 校验ret,确保安全函数执行成功 */
Log(error_msg);
}
推荐做法1:使用不同的源和目标缓冲区来实现复制功能。
void LogMessageItem(int error_type, char * error_msg)
{
char tmp_msg[MAX_MESSAGE_SIZE + 1] = {
0x00};
int ret = sprintf_s(tmp_msg, sizeof(tmp_msg), "%d:%s",error_type,error_msg);
/* 【修改】分配另一块内存来避免内存重叠 */
/* 校验ret,确保安全函数执行成功 */
Log(tmp_msg);
}
错误示例2:
unsigned char *p1 = GetCurrentMessage();
unsighed char *p2 = p1 + KEY_FIELD_OFFSET; /* 【错误】 p1和p2存在重叠 */
int ret = memcpy_s(p2, MAX_SIZE, p1,KEY_FIELD_SIZE); /* 【错误】当p1和p2存在重叠时,memcpy_s()不能实现其功能 */
/* 校验ret,确保安全函数执行成功 */
推荐做法2:使用 memmove_s 函数,源字符串和目标字符串所指内存区域可以重叠,但复制后目标字符串内容会被更改,该函数将返回指向目标字符串的指针。
unsigned char * p1 = GetCurrentMessage();
unsigned char * p2 = p1 + KEY_FIELD_OFFSET;
int ret = memmove_s(p2, MAX_SIZE, p1, KEY_FIELD_SIZE); /* 【修改】使用memmove_s来代替memcpy_s,来避免内存重叠带来的问题 */
/* 校验ret,确保安全函数执行成功 */
memcpy_s
与memmove_s
的目的都是将N个字节的源内存地址的内容拷贝到目标内存地址中。但当源内存和目标内存存在重叠时,memcpy_s
不会实现其功能,而memmove_s
能正确地实施拷贝,但这也增加了一点点开销。
格式化输出安全
规则C2.1:禁止以用户输入来构造格式化字符串
说明:调用格式化函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。如果攻击者对一个格式化字符串可以部分或完全控制,将导致进程崩溃、查看栈的内容、改写内存、甚至执行任意代码等风险。这些格式化函数有:
- 格式化输出函数:printf(),fprintf(),sprintf(),snprintf(),vprintf(), vfprintf(),vsprintf(),vsnprintf(),asprintf()(GNU扩展函数),vasprintf()(GNU扩展函数)及相应宽字节版本;
- 格式化输入函数:scanf(),fscanf(),sscanf(),vscanf(),vsscanf(),vfscanf()及相应宽字节版本;
- 格式化错误消息函数:err(),verr(),errx(),verrx(),warn(),vwarn(),warnx(),vwarnx(),error(),error_at_line();
- 格式化日志函数:syslog(),vsyslog()。
备注:以上部分函数是禁用的,详细请参考规则C5.1。
错误示例: 代码中incorrect_password()函数的功能是当某个用户的用户名没有找到或者口令不正确时,展示一条错误消息。这个函数接受来自用户的参数user,而这个用户是未经过认证的,也就是不安全的外部输入。函数将user构造成一个告警展示信息,然后通过fprintf()将该信息打印到stderr中。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void incorrect_password(const char *user)
{
int ret = 0;
/* 用户名称被限定在256个字节以内 */
static const char msg_format[] = "%s cannot be authenticated.\n";
size_t len = strlen(user) + sizeof(msg_format);
if(len > MAX_LEN)
{
/* 处理错误 */
}
char *msg = (char *)malloc(len);
if (NULL == msg)
{
/* 处理错误*/
}
ret = sprintf_s(msg, len, msg_format, user);
if (ret < 0)
{
/* 处理错误 */
}
else if (ret >= len)
{
/* 处理截断输出 */
}
fprintf(stderr, msg); /* 【错误】格式化错误产生 */
free(msg);
}
示例代码中首先计算了消息的长度,然后分配内存,接着利用sprintf_s()函数构造消息内容。因此消息内容中包含了msg_format的内容和用户的内容,当用户输入大量的格式符(如%s,%p等)后,fprintf()在执行时,会将msg作为一个格式化字符串来进行解析,而不是消息内容,也就是说msg不会被打印到stderr中,反而会将一些未知的数据打印其中,会引发程序崩溃等未定义的行为。这是一个非常严重的格式化漏洞。
推荐做法1:下列代码使用fputs()来代替fprintf()函数,fputs()会直接将msg的内容输出到stderr中,而不会去解析它。
void incorrect_password(const char *user)
{
int ret = 0;
/*用户名称被限定在256个字节以内*/
static const char msg_format[] = "%s cannot be authenticated.\n";
size_t len = strlen(user) + sizeof(msg_format);
if(len > MAX_LEN)
{
/* 处理错误 */
}
char *msg = (char *)malloc(len);
if (NULL == msg)
{
/* 处理错误 */
}
ret = sprintf_s(msg, len, msg_format, user);
if (ret < 0)
{
/* 处理错误 */
}
else if (ret >= len)
{
/* 处理截断错误 */
}
if (fputs(msg, stderr) == EOF) /* 【修改】使用fputs()函数代替fprintf() */
{
/* 处理错误 */
}
free(msg);
}
推荐做法2:通过格式说明符“%s”将user以字符串的形式固定下来然后输出到stderr中。
void incorrect_password(const char *user)
{
static const char msg_format[] = "%s cannot be authenticated.\n"; /* 【修改】使用“%s”来限定msg的格式,使msg不会被解析 */
fprintf(stderr, msg_format, user);
}
整数安全
规则C3.1:确保无符号整数运算时不会出现反转
说明:反转是指无法用无符号整数表示的运算结果将会根据该类型可以表示的最大值加1执行求模操作。常见的可能引起反转的操作符有:
+、-、*、++、--、+=、-=、*=、<<=、<<、-
来自于系统外部或其它不可信数据参与到上述运算中的情形,只要将运算结果用于以下之一(包括但不局限于)的用途,都应该添加校验以防止反转:
- 作为数组索引
- 指针运算
- 作为对象的长度或者大小
- 作为数组的边界
- 作为内存分配函数的实参
- 作为循环终止判定条件
- 作为拷贝长度
在上述校验场景中,若代码执行前能确定运算结果不会反转的,可以不作校验,如:
- 两个静态常量(compile-time constants)操作;
- 与0进行运算(除数不能为0);
- 任意类型的最大值减法(如UINT_MAX减去任意无符号数都是安全的);
- 任何变量乘1操作;
- 除法或者取余操作中,只要保证除数不为0;
- 右移运算时,右移位数不超过该无符号整数类型的精度,如UNIT_MAX >> x中,只要 0 <= x < 32就是合法的(假设unsigned int类型的精度是32位)。
错误示例:下列代码可能导致相加操作产生无符号数反转现象。
static int handlehdr_odc(aim_session_t * sess , aim_...)
{
...
unsigned int payloadlength = aimbs_get32(bs);
/* payloadlength is read from an untrusted source*/
if(!(msg = calloc(1,payloadlength + 1))) /*【错误】payloadlength + 1未校验,可能反转为0,导致内存申请参数为0 */
/* potential overflow */
{
...
}
while(payloadlength - recvd)
{
if(payloadlength - recvd >= 1024)
{
i = aim_recv(conn->fd,&msg[recvd],1024); /*msg申请内存为0,导致读取数据消息失败 */
}
else
{
...
}
}
}
推荐做法:在运算之前添加校验,确保不会产生无符号数反转。
static int handlehdr_odc(aim_session_t * sess , aim_...)
{
...
unsigned int payloadlength = aimbs_get32(bs);
/* payloadlength is read from an untrusted source*/
if(payloadlength == 0 || payloadlength > (MAX_SIZE - 1)) /*【修改】确保payloadlength合法 */
{
/* 错误处理 */
}
if(!(msg = calloc(1,payloadlength + 1)))
/* potential overflow */
{
...
}
while(payloadlength - recvd)
{
if(payloadlength - recvd >= 1024)
{
i = aim_recv(conn->fd,&msg[recvd],1024);
}
else
{
...
}
}
}
规则C3.2:确保有符号整数运算时不会出现溢出
说明:整数溢出是是一种未定义的行为,意味着编译器在处理有符号整数溢出时具有很多选择。以下是可能引起整数溢出的常见操作符:
+ 、-= 、- 、*=、*、/=、/、%=、%、<<=、++、<<、--、-、+=
来自于系统外部或其它不可信数据参与到上述运算中的情形,只要将运算结果用于以下之一(包括但不局限于)的用途,都应该添加校验以防止溢出:
- 作为数组索引
- 指针运算
- 作为对象的长度或者大小
- 作为数组的边界
- 作为内存分配函数的实参
- 作为循环终止判定条件
- 作为拷贝长度
错误示例:下列代码中两个有符号整数相加可能会产生溢出。
static int handlehdr_odc(aim_session_t * sess , aim_...)
{
...
char payloadlength = aimbs_get32(bs);
/* payloadlength is read from an untrusted source*/
...
if(!(msg = calloc(1,payloadlength + 1))) /*【错误】payloadlength + 1未校验,当payloadlength为127时,则溢出为-128,calloc()函数会将其转为非常大的正整数,可导致内存申请失败 */
{
...
}
while(payloadlength - recvd)
{
if(payloadlength - recvd >= 1024)
{
i = aim_recv(conn->fd,&msg[recvd],1024);
}
else
...
}
}
推荐做法:在运算之前添加校验,确保不会产生有符号溢出。
static int handlehdr_odc(aim_session_t * sess , aim_...)
{
...
char payloadlength = aimbs_get32(bs);
/* payloadlength is read from an untrusted source*/
if(payloadlength <= 0 || payloadlength > (CHAR_MAX - 1)) /*【修改】确保payloadlength合法,其中CHAR_MAX = 127 */
{
/* 错误处理 */
}
if(!(msg = calloc(1,payloadlength + 1)))
{
...
}
while(payloadlength - recvd)
{
if(payloadlength - recvd >= 1024)
{
i = aim_recv(conn->fd,&msg[recvd],1024);
}
else
...
}
}
规则C3.3:确保整型转换时不会出现截断错误
说明: 将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。
截断错误会引起数据丢失,甚至可能引发安全问题。特别是将运算结果用于以下用途:作为数组索引、指针运算、作为对象的长度或者大小、作为数组的边界(如作为循环计数器)
错误示例:数据类型强制转化导致数据被截断。
void func(void)
{
signed long int s_a = LONG_MAX;
signed char sc = (signed char)s_a; /* 【错误】不同类型强制转化会发生数据截断 */
/* ... */
}
推荐做法:当不同数据类型强制转化时需要首先校验数据的范围,以确定是否会发生数据的丢失。
void func(void)
{
signed long int s_a = LONG_MAX;
signed char sc;
if ((s_a < SCHAR_MIN) || (s_a > SCHAR_MAX)) /* 【修改】进行校验以确保在进行类型转化时不会产生截断 */
{
/* 处理错误 */
}
else
{
sc = (signed char)s_a; /* Use cast to eliminate warning */
}
/* ... */
}
规则C3.4:确保有符号数和无符号数之间的转换符合预期
说明:有符号数和无符号数之间的转换包括:有符号数到无符号数的转换和无符号数到有符号数的转换。
将转换结果用于敏感用途(如作为数组索引、指针运算、对象的长度或大小、数组边界、内存分配函数实参等)时,一定要确保转换结果在自己的预期内,否则极易引发安全问题:
- 有符号数转换成无符号数:若有符号数为一个负数,那么转成无符号数时,将会是一个非常大的正整数。
- 无符号数转成有符号数:若无符号数为一个较大的数,那么转成有符号数时,可能会转换成一个负数。
错误示例1:使用有符号数,且未完整校验就作为内存申请函数实参。
DataPacket *packet = NULL;
int numHeaders = 0; /* numHeader定义为有符号数 */
PacketHeader *headers = NULL;
...
sock = AcceptSocketConnection();
ReadPacket(packet, sock);
numHeaders = packet->headers;
if (numHeaders > 100) /* 只校验numHeader的上限值,未校验其下限值 */
{
ExitError("too many headers!");
}
headers = malloc(numHeaders * sizeof(PacketHeader)); /* 【错误】 当numberHeader负数时,malloc申请的内存可能过大*/
ParsePacketHeaders(packet, headers);
上述代码中,numHeaders被定义为signed int类型,且来自外部输入,校验时只校验了上限值,却未校验下限值,当其为负数时,便会产生问题。比如,若numHeaders为-1,sizeof(PacketHeader)为10,那么二者的运算为-10,当其作为malloc()入参时,malloc()会将其转化为一个非常大的无符号数(即4294967286),这样的入参,可能会让malloc()执行失败,导致程序崩溃。也可能因此申请过大内存,导致资源耗尽。
推荐做法1:添加校验,确保numHeaders不为负数,且malloc()入参符合预期。
DataPacket *packet = NULL;
int numHeaders = 0; /* numHeader定义为有符号数 */
PacketHeader *headers = NULL;
...
sock = AcceptSocketConnection();
ReadPacket(packet, sock);
numHeaders = packet->headers;
if (numHeaders > 100 || numHeaders < 0) /* 【修改】添加校验,确保numHeaders不为负 */
{
ExitError("too many headers!");
}
unsigned int mallocSize = numHeaders * sizeof(PacketHeader);
if (mallocSize == 0|| mallocSize >= MAX_SIZE) /* 【修改】添加malloc入参校验,确保不为0,不超过最大申请值,即结果符合预期 */
{
ExitError("malloc size error");
return;
}
headers = malloc(numHeaders * sizeof(PacketHeader));
ParsePacketHeaders(packet, headers);
错误示例2:使用有符号数,且未完整校验即作为拷贝长度,导致缓冲区溢出。
void main (int argc, char **argv)
{
char path[256] = {
0x0};
char *input = NULL;
int Length = 0;
Length = GetUntrustedInt(); /* Length来自外部不可信输入,可能为负 */
if (Length > 256) /* 只校验上限,未校验下限值 */
{
DiePainfully("go away!\n");
return;
}
input = GetUserInput("Enter pathname:");
strncpy(path, input, Length); /* 【错误】若length为负数,那么此处会被转换成一个非常大的正整数,导致path缓冲区溢出 */
path[255] = '\0'; /* 添加结束符 */
printf("Path is: %s\n", path);
}
推荐做法2:添加校验,确保拷贝长度不为负数。
void main (int argc, char **argv)
{
char path[256] = {
0x0};
char *input = NULL;
int Length = 0;
Length = GetUntrustedInt(); /* Length来自外部不可信输入,可能为负 */
if (Length > 256 || Length <= 0) /* 【修改】添加下限校验,确保不为负数 */
{
DiePainfully("go away!\n");
return;
}
input = GetUserInput("Enter pathname:");
strncpy(path, input, Length);
path[255] = '\0'; /* 添加结束符 */
printf("Path is: %s\n", path);
}