当前位置:   article > 正文

C/C++语言的安全编码规范_华为c&c++安全编码规范

华为c&c++安全编码规范

在很多领域,如网络安全、汽车智能驾驶等都非常注重代码安全,这是确保产品质量,减少产品事故的关键环节,很多大公司例如华为等都对产品的安全编码有着极高要求,并有相应的一套完整编程规范来引导,少数对产品质量要求极高的公司如华为还会有相应的安全编码考试,以此培养员工的安全编码思想和意识。安全编码不仅关乎产品质量,在某些场景下更是避免软件灾难的最重要防线,对于从事软件开发的人员来说,安全编码思想的重要性不言而喻。

一、安全编码的基本思想

开发人员在安全编码过程中应该保持如下的假设:

1、程序所处理的所有外部数据都是不可信的攻击数据;

外部数据定义如下:

  • 文件
  • 注册表
  • 网络
  • 环境变量
  • 命令行
  • 用户输入
  • 用户态数据
  • 进程间通信
  • 函数参数(对于API)
  • 全局变量(其他线程会修改全局变量)

2、攻击者时刻试图监听、篡改、破坏程序的运行环境、外部数据。

基于以上假设,得出安全编码基本思想:

(1)程序在处理外部数据时必须经过严格的合法性校验;

(2)尽量减少代码的攻击面。即代码的实现应该尽量简单,避免与外部环境做多余的数据交互。

(3)通过防御性的编码策略来弥补潜在的编码人员的疏忽。这些措施包括:

  • 变量声明应该赋初值
  • 谨慎使用全局变量
  • 避免使用功能复杂、易用错的函数
  • 禁用易用错的编码器的机制
  • 小心处理资源访问过程
  • 不要改变操作系统的运行环境
  • 合理使用调试断言(ASSERT)
  • 严格的错误处理

二、安全编码的要求

安全编码的内容非常广泛,涉及变量、断言、函数、循环、异常机制、类、安全退出、字符串/数组操作、整数、内存、不安全函数、文件输入/输出、敏感信息处理等各方面,这里简略叙述最重要的内存、函数、字符串处理等几个模块,以期能达到举一反三作用,引导大家树立安全编码思想。

1、内存

在C/C++编码中,内存的使用要非常谨慎,因为内存结合指针使用时非常灵活,稍微使用不当就可能造成段错误或内存泄漏等严重问题。内存的安全编码应该遵循如下要求:

(1)内存申请前,必须对申请内存的大小进行合法性校验

内存申请的大小值可能来自外部数据,必须检查其合法性,防止过多的、非法的申请内存。不能申请长度为0的内存。

  1. int Foo(int size)
  2. {
  3. if (size <= 0) {
  4. //error
  5. ...
  6. }
  7. ...
  8. char *msg = (char *)malloc(size);
  9. if (msg == NULL) {
  10. ...
  11. }
  12. ...
  13. }

(2)内存分配后必须判断是否成功

如下

  1. char *msg = (char *)malloc(size);
  2. if (msg == NULL) {
  3. ...
  4. }

(3)禁止引用未初始化的内存

malloc、new分配出来的内存没有被初始化为0,要确保内存被引用前是被初始化的。

可使用memset来初始化申请的内存,如下

  1. int *CalcMetrixColomn(int **metrix, int *param, int size)
  2. {
  3. int *result = NULL;
  4. ...
  5. int bufsize = size * sizeof(int);
  6. ...
  7. result = (int *)malloc(bufsize);
  8. ...
  9. int ret = memset(result, 0, bufsize); // 确保内存被初始化后才被引用
  10. ...
  11. result[0] += metrux[0][0] * param[0];
  12. ...
  13. return result;
  14. }

(4)内存释放之后要赋予新值

内存释放之后,如果其指针未立即设置未NULL,也未分配一个新的对象,那么可能会导致该指针在后续代码中产生双重释放的风险,还存在访问已释放内存的危险。

如下

  1. char *msg = NULL;
  2. ...
  3. msg = (char *)malloc(len);
  4. ...
  5. if (...) {
  6. free(msg); // 在此分支对内存进行了释放
  7. msg = NULL; // 释放后要立即将指针赋值为NULL,否则会为后续代码带来风险
  8. }
  9. ...
  10. if (msg != NULL) {
  11. free(msg);
  12. msg = NULL;
  13. }

(5)禁止使用realloc函数

realloc函数原型如下:

void *realloc(void *p, int size);

该函数随着参数的不同,其行为也不一样,也就是一个函数被赋予了多种不同行为。这不是一个设计良好的函数,极易引发各种bug。

(6)禁止使用alloca函数申请栈上内存

该函数在有些平台下不支持,使用alloca函数会降低程序的兼容性和可移植性。该函数在栈帧里申请内存,申请的大小很可能超过栈的边界,影响后续的代码执行。

2、函数

(1)数组作为函数参数时,必须同时将其长度作为函数的参数

通过函数参数传递数组或一块内存进行写操作时,函数参数必须同时传递数组元素个数或所传递的内存块大小,否则函数在使用数组下标或访问内存偏移时,无法判断下标或便宜的合法范围,产生越界访问的漏洞。

如下

  1. int ParseMsg(BYTE *msg, int msgLen)
  2. {
  3. ASSERT(msg != NULL);
  4. ASSERT(msgLen != 0);
  5. ...
  6. }
  7. ...
  8. int len = ...
  9. BYTE *msg = (BYTE *)malloc(len);
  10. ....
  11. ParseMsg(msg, len); // 将msg的大小作为参数传递到函数中
  12. ....

以上代码msg为申请的内存块,对于固定长度的数组,也必须将其大小作为函数的参数传入。

对于const char *类型的参数,它的长度是通过'\0'的位置计算出来,不需要传长度参数。但如果是char *类型,且参数作为写内存的缓冲区,则需要传长度参数。

(2)不对内容进行修改的指针型参数,定义为const

如果参数是指针型类型,且内容不会被修改,应定义为const类型。

  1. int Foo(const char *filePath)
  2. {
  3. ...
  4. int fd = open(filePath, ...);
  5. ...
  6. }

(3)谨慎使用不可重入函数

不可重入函数在多线程环境下,其执行结果不能达到预期效果,需谨慎使用。常见的不可重入函数包括:rand、srand、getenv、getenv_s、strtok、strerro、setlocale、atomic_init、tmpnam、gethostbyaddr、gethostbyname等。

(4)字符串或指针作为函数参数时,请检查参数是否为NULL

如果字符串或者指针作为函数参数,为了防止空指针引用错误,在引用前必须确保该参数不为NULL,如果上层调用者已经保证了该参数不可能为NULL,在调用本函数时,在函数开始处可以加ASSERT进行校验。 例如下面的代码,因为BYTE *p有可能为NULL,因此在使用前需要进行判断。

  1. int Foo(int *p, int count)
  2. {
  3. if (p != NULL && count > 0) {
  4. int c = p[0];
  5. }
  6. ...
  7. }
  8. int Foo2()
  9. {
  10. int *arr = ...
  11. int count = ...
  12. Foo(arr, count);
  13. ...
  14. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/544969
推荐阅读
相关标签
  

闽ICP备14008679号