c++编码规范(一)

1 通用原则 7
原则1.1:对外部输入进行校验 7
原则1.2:禁止在日志中保存口令、密钥 8
原则1.3:及时清除存储在可复用资源中的敏感信息 8
原则1.4:正确使用经过验证的安全的标准加密算法 8
原则1.5:遵循最小权限原则 9
原则1.6:删除或修改没有效果的代码 9
原则1.7:删除或修改没有使用到的变量或值 9
2 字符串操作安全 10
规则2.1:确保有足够的空间存储字符串的字符数据和’\0’结束符 10
规则2.2:字符串操作过程中确保字符串有’\0’结束符 11
规则2.3:把数据复制到固定长度的内存前必须检查边界 13
规则2.4:避免字符串/内存操作函数的源指针和目标指针指向内存重叠区 13
3 格式化输出安全 15
规则3.1:格式化输出函数的格式化参数和实参类型必须匹配 15
规则3.2:格式化输出函数的格式化参数和实参个数必须匹配 17
规则3.3:禁止以用户输入来构造格式化字符串 17
建议3.1:使用格式化函数时推荐使用精度说明符 18
4 整数安全 19
规则4.1:禁止无符号整数运算时出现反转 19
规则4.2:禁止有符号整数运算时出现溢出 20
规则4.3:禁止整型转换时出现截断错误 21
规则4.4:禁止整型转换时出现符号错误 22
规则4.5:把整型表达式比较或赋值为一种更大类型之前必须用这种更大类型对它进行求值 23
建议4.1:避免对有符号整数进行位操作符运算 23
5 内存管理安全 24
规则5.1:禁止引用未初始化的内存 24
规则5.2:禁止访问已经释放的内存 25
规则5.3:禁止重复释放内存 27
规则5.4:必须对指定申请内存大小的整数值进行合法性校验 27
规则5.5:禁止释放非动态申请的内存 29
建议5.1:避免使用alloca函数申请内存 29
6 禁用不安全函数或对象 30
规则6.1:禁止使用未显式指明目标缓冲区大小的字符串操作函数 30
规则6.2:禁止调用OS命令解析器执行命令或运行程序,防止命令注入 32
规则6.3:禁止使用std::ostrstream,推荐使用std::ostringstream 33
规则6.4:C++中,必须使用C++标准库替代C的字符串操作函数 33
7 文件输入/输出安全 35
规则7.1:必须使用int类型来接收字符输入/输出函数的返回值 35
规则7.2:创建文件时必须显式指定合适的文件访问权限 36
规则7.3:文件路径验证前,必须对其进行标准化 36
建议7.1:访问文件时尽量使用文件描述符代替文件名作为输入,以避免竞争条件问题 37
8 STL库安全 38
规则8.1:引用容器前后元素时要确保容器元素存在 38
规则8.2:迭代子使用前必须保证迭代子有效 39
规则8.3:必须确保迭代子指向的内容有效 40
规则8.4:正确处理容器的erase()方法与迭代子的关系 41
9 C++类和对象安全 42
规则9.1:禁止切分多态的类对象 42
规则9.2:禁止定义基类析构函数为非虚函数,所有可能被继承类的析构函数都必须定义为virtual 44
规则9.3:避免出现delete this操作 46
规则9.4:禁止在类的公共接口中返回类的私有数据地址 48
建议9.1:重载后缀操作符应返回const类型 49
建议9.2:显式声明的模板类应进行类型特化 50
10 其它 51
规则10.1:禁止使用rand()产生用于安全用途的伪随机数 51
规则10.2:禁止存储getenv()返回的字符串指针 55
规则10.3:多线程环境下,禁止使用可能会导致crash的不安全函数 57
建议10.1:编译时应当使用编译器的最高警告等级 60
建议10.2:防止处理敏感数据的代码因被编译器优化而失效 60
 

0.4 术语定义
原则: 编程时必须遵守的指导思想。
规则:编程时必须遵守的约定。
建议:编程时必须加以考虑的约定。
说明:对此原则/规则/建议进行必要的解释。
错误示例:对此原则/规则/建议从反面给出例子。
推荐做法:对此原则/规则/建议从正面给出例子。
延伸阅读材料:建议进一步阅读的参考材料。
1 通用原则
原则1.1:对外部输入进行校验
说明:对于外部输入(包括用户输入、外部接口输入、配置文件、网络数据和环境变量等)可能用于以下场景的情况下,需要检验入参的合法性:
 输入会改变系统状态
 输入作为循环条件
 输入作为数组下标
 输入作为内存分配的尺寸参数
 输入作为格式化字符串
 输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
 输入影响代码逻辑
这些情况下如果不对用户数据作合法性验证,很可能导致DoS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。
对外部输入验证常见有如下几种方式:
(1)校验输入数据长度:
如果输入数据是字符串,通过校验输入数据的长度可以加大攻击者实施攻击的难度,从而防止缓冲区溢出、恶意代码注入等漏洞。
(2)校验输入数据的范围:
如果输入数据是数值,必须校验数值的范围是否正确,是否合法、在有效值域内,例如在涉及到内存分配、数组操作、循环条件、计算等安全操作时,若没有进行输入数值有效值域的校验,则可能会造成内存分配失败、数组越界、循环异常、计算错误等问题,这可能会被攻击者利用并进行进一步的攻击。
(3)输入验证前,对数据进行归一化处理以防止字符转义绕过校验:
通过对输入数据进行归一化处理(规范化,按照常用字符进行编码),彻底去除元字符,可以防止字符转义绕过相应的校验而引起的安全漏洞。
(4)输入校验应当采用“白名单”形式:
“黑名单”和“白名单”是进行数据净化的两种途径。“黑名单”尝试排斥无效的输入,而“白名单”则通过定义一个可接受的字符列表,并移除任何不接受的字符来仅仅接受有效的输入。有效输入值列表通常是一个可预知的、定义良好的集合,并且其大小易于管理。
“白名单”的好处在于,程序员可以确定一个字符串中仅仅包含他认为安全的字符。
“白名单”比“黑名单”更受推荐的原因是,程序员不必花力气去捕捉所有不可接受的字符,只需确保识别了可接受的字符就可以了。这样一来,程序员就不用绞尽脑汁去考虑攻击者可能尝试哪些字符来绕过检查。
原则1.2:禁止在日志中保存口令、密钥
说明:在日志中不能保存口令和密钥,其中的口令包括明文口令和密文口令。对于敏感信息建议采取以下方法,
 不打印在日志中;
 若因为特殊原因必须要打印日志,则用“*”代替。
原则1.3:及时清除存储在可复用资源中的敏感信息
说明:存储在可复用资源中的敏感信息如果没有正确的清除则很有可能被低权限用户或者攻击者所获取和利用。因此敏感信息在可复用资源中保存应该遵循存储时间最短原则。可复用资源包括以下几个方面:
 堆(heap)
 栈(stack)
 数据段(data segment)
 数据库的映射缓存
存储口令、密钥的变量使用完后必须显式覆盖或清空。
原则1.4:正确使用经过验证的安全的标准加密算法
说明:禁用私有算法或者弱加密算法(如DES,SHA等),应该使用经过验证的、安全的、公开的加密算法。
加密算法分为对称加密算法和非对称加密算法。推荐使用的常用对称加密算法有:
 AES
推荐使用的常用非对称算法有:
 RSA
 数字签名算法(DSA)
此外还有验证消息完整性的安全哈希算法(SHA256)等。基于哈希算法的口令安全存储必须加入盐值(salt)。
密钥长度符合最低安全要求:
 AES:  128位
 RSA:  2048位
 DSA:  1024位
 SHA:  256位
原则1.5:遵循最小权限原则
说明:程序在运行时可能需要不同的权限,但对于某一种权限不需要始终保留。例如,一个网络程序可能需要超级用户权限来捕获原始网络数据包,但是在执行数据报分析等其它任务时,则可能不需要相同的权限。因此程序在运行时只分配能完成其任务的最小权限。过高的权限可能会被攻击者利用并进行进一步的攻击。
(1)撤销权限时应遵循正确的撤销顺序:
在涉及到set-user-ID和set-group-ID程序中,当有效的用户ID(user ID)和组ID(group ID)与真实的用户不同时,不但要撤销用户层面(user level)的权限而且要撤销组层面(group level)的权限。在进行这样的操作时,要保证撤销顺序的正确性。
权限撤销顺序的不正确操作,可能会被攻击者获得过高的权限而进行进一步的攻击。
(2) 完成权限撤销操作后,应确保权限撤销成功:
不同平台下所谓的“适当的权限”的意义是不相同的。例如在Solaris中,setuid()的适当的权限指的是PRIV_PROC_SETID权限在进程的有效权限集中。在BSD中意味着有效地用户ID(EUID)为0或者uid=geteuid()。而在Linux中,则是指进程具有CAP_SETUID能力并且当EUID不等于0、真正的用户ID(RUID)或者已保存的set-user ID(SSUID)中任何一个时,setuid(geteuid())是失败的。
正是由于权限行为的复杂性,所以所需的权限在撤销时可能会失败。这会被攻击者利用并进行进一步的攻击。例如Kernel版本在2.2.0-2.2.15的Linux就有一个权限撤销漏洞,当权限功能位置为0时,setuid(getuid())没有如预期的那样撤销权限成功。因此在进行权限撤销操作后,应该校验以保证权限撤销成功。
原则1.6:删除或修改没有效果的代码
说明:删除或修改一些即使执行后、也不会有任何效果的代码。
一些存在的代码(声明或表达式),即使它被执行后,也不会对代码的结果或数据的状态产生任何的影响,或者产生不是所预期的效果,这样的代码在可能是由于编码错误引起的,往往隐藏着逻辑上的错误。
原则1.7:删除或修改没有使用到的变量或值
说明:删除或修改没有使用到的变量或值。一些变量或值存在于代码里,但并没有被使用到,这可能隐含着逻辑上的错误,需要被识别出来,删除这类语句或做相应的修改。
2 字符串操作安全
规则2.1:确保有足够的空间存储字符串的字符数据和’\0’结束符
说明:在分配内存或者在执行字符串复制操作时,除了要保证足够的空间可以容纳字符数据,还要预留’\0’结束符的空间,否则会造成缓冲区溢出。
错误示例1:拷贝字符串时,源字符串长度可能大于目标数组空间。
void main(int argc, char *argv[])
{
 char dst[128];
 if ( argc > 1 )
 {
  strcpy(dst, argv[1]);  // 源字符串长度可能大于目标数组空间,造成缓冲区溢出
 }
 /*…*/
}
推荐做法:根据源字符串长度来为目标字符串分配空间。
void main(int argc, char *argv[])
{
 char *dst = NULL;
 if ( argc > 1 )
 {
  dst = (char *)malloc(strlen(argv[1]) + 1); /* 【修改】确保字符串空间足够容纳argv[1] */
  if( dst != NULL )
  {
   strncpy(dst, argv[1], strlen(argv[1]));
   dst[strlen(argv[1])] = ’\0’; //【修改】dst以’\0’结尾
  }
 }
 /*...dst使用后free...*/
}
错误示例2 :典型的差一错误,未考虑’\0’结束符写入数组的位置,造成缓冲区溢出和内存改写。
void NoCompliant()
{
 char dst[ARRAY_SIZE + 1];
 char src[ARRAY_SIZE + 1];
 unsigned int i = 0;
 memset(src, '@', sizeof(dst));
 for(i=0; src[i] != ’\0’ && (i < sizeof(dst)); ++i )
  dst[i] = src[i];
 dst[i] = ’\0’;
 /*…*/
}
推荐做法:
void Compliant()
{
 char dst[ARRAY_SIZE + 1];
 char src[ARRAY_SIZE + 1];
 unsigned int i = 0;
 memset(src, '@', sizeof(dst));
 for(i=0; src[i]!=’\0’ && (i < sizeof(dst) - 1 ); ++i) /*【修改】考虑’\0’结束符 */
  dst[i] = src[i];
 dst[i] = ’\0’;
 /*…*/
}
规则2.2:字符串操作过程中确保字符串有’\0’结束符
说明:字符串结束与否是以’\0’作为标志的。没有正确地使用’\0’结束字符串可能导致字符串操作时发生缓冲区溢出。因此对于字符串或字符数组的定义、设置、复制等操作,要给’\0’预留空间,并保证字符串有’\0’结束符。
注意:strncpy、strncat等带n版本的字符串操作函数在源字符串长度超出n标识的长度时,会将包括’\0’结束符在内的超长字符串截断,导致’\0’结束符丢失。这时需要手动为目标字符串设置’\0’结束符。
错误示例1:strlen()不会将’\0’结束符算入长度,配合memcpy使用时会丢失’\0’结束符。
void  Noncompliant()
{
 char dst[11];
 char src[] = "0123456789";
 char *tmp = NULL;
 memset(dst, '@', sizeof(dst));
 memcpy(dst, src, strlen(src));
 printf("src: %s \r\n", src);
 tmp = dst;  //到此,dst还没有以’\0’结尾
 do
 {
  putchar(*tmp);
 }while (*tmp++);    // 访问越界
 return;
}
推荐做法: 为目标字符串设置’\0’结束符
void  Compliant()
{
 char dst[11];
 char src[] = "0123456789";
 char *tmp = NULL;
 memset(dst, '@', sizeof(dst));
 memcpy(dst, src, strlen(src));
 dst[sizeof(dst) - 1] = ’\0’;  //【修改】dst以’\0’结尾
 printf("src: %s \r\n", src);
 tmp = dst;
 do
 {
  putchar(*tmp);
 } while (*tmp++);
 return;
}
错误示例2:strncpy()拷贝限长字符串,截断了’\0’结束符。
void  Noncompliant()
{
 char dst[5];
 char src[] = "0123456789";
 strncpy(dst, src, sizeof(dst));
     printf(dst); //访问越界,dst没有’\0’结束符
 return;
}
推荐做法:
void  Compliant()
{
 char dst[5];
 char src[] = "0123456789";
 strncpy(dst, src, sizeof(dst));
 dst[sizeof(dst)-1] = ’\0’;  // 【修改】最后字节置为’\0’
     printf(dst);
 return;
}
规则2.3:把数据复制到固定长度的内存前必须检查边界
说明:将未知长度的数据复制到固定长度的内存空间可能会造成缓冲区溢出,因此在进行复制之前应首先获取并检查数据长度。典型的如来自gets()、getenv()、scanf()的字符串。
错误示例:环境变量长度不可预测,不加检查的复制会造成缓冲区溢出。
void  Noncompliant()
{
 char dst[16];
 char * temp = getInputMsg();
 if(temp != NULL)
 {
  strcpy(dst,temp); // temp长度可能超过dst的大小
 }
 return;
}
推荐做法:
void  Compliant()
{
 char dst[16];
 char *temp = getInputMsg();
 if(temp != NULL)
 {
  strncpy(dst, temp, sizeof(dst)); /* 【修改】只复制不超过数组dst大小的数据 */
 }
 dst[sizeof(dst) -1] = ’\0’; //【修改】copy以’\0’结尾
 return;
}
规则2.4:避免字符串/内存操作函数的源指针和目标指针指向内存重叠区
说明:内存重叠区是指一段确定大小及地址的内存区,该内存区被多个地址指针指向或引用,这些指针介于首地址和尾地址之间。
在使用像memcpy、strcpy、strncpy、sscanf()、sprintf()、snprintf()和wcstombs()这样的函数时,复制重叠对象会存在未定义的行为,这种行为可能破坏数据的完整性。
错误示例1:snprintf的参数使用存在问题
void  Noncompliant()
{
#define MAX_LEN 1024
 char cBuf[MAX_LEN + 1] = {0};
 int nPid = 0;
 strncpy(cBuf, ”Hello World!”, strlen(”Hello World!”));
 snprintf(cBuf, MAX_LEN, "%d: %s", nPid, cBuf); /* cBuf既是源又是目标,函数使用不安全 */
 return;
}
推荐做法:使用不同源和目标缓冲区来实现复制功能。
void  Compliant()
{
#define MAX_LEN 1024
 char cBuf[MAX_LEN + 1] = {0};
 char cDesc[MAX_LEN + 1] = {0}; //【修改】另起一个缓冲区,防止缓冲区重叠出错
 int nPid = 0;
 strncpy(cDesc, ”Hello World!”, strlen(”Hello World!”)); /* 【修改】防止缓冲区重叠出错 */
 snprintf(cBuf, MAX_LEN, "%d: %s", nPid, cDesc); /* 【修改】防止缓冲区重叠出错 */
 return;
}
错误示例2:
#define MSG_OFFSET 3
#define MSG_SIZE 6
void  NoCompliant ()
{
 char str[] = "test string";
 char *ptr1 = str;
 char *ptr2;
 ptr2 = ptr1+MSG_OFFSET;
 memcpy(ptr2, ptr1, MSG_SIZE);
 return;
}
推荐做法:使用memmove函数,源字符串和目标字符串所指内存区域可以重叠,但复制后目标字符串内容会被更改,该函数将返回指向目标字符串的指针。
#define MSG_OFFSET 3
#define MSG_SIZE 6
void  Compliant ()
{
 char str[] = "test string";
 char *ptr1 = str;
 char *ptr2;
 ptr2 = ptr1 + MSG_OFFSET;
 memmove(ptr2, ptr1, MSG_SIZE); /*【修改】使用memmove代替memcpy,防止缓冲区重叠出错 */
 return;
}
memcpy与memmove的目的都是将N个字节的源内存地址的内容拷贝到目标内存地址中。
但当源内存和目标内存存在重叠时,memcpy会出现错误,而memmove能正确地实施拷贝,但这也增加了一点点开销。
memmove的处理措施:
 当源内存的首地址等于目标内存的首地址时,不进行任何拷贝
 当源内存的首地址大于目标内存的首地址时,实行正向拷贝
 当源内存的首地址小于目标内存的首地址时,实行反向拷贝

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页