• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# OpenHarmony C&C++ 安全编程指南
2
3本文档基于C&C++ 语言提供一些安全编程建议,用于指导开发实践。
4
5# 函数
6
7## 对所有外部数据进行合法性校验
8
9**【描述】**
10外部数据的来源包括但不限于:网络、用户输入、命令行、文件(包括程序的配置文件)、环境变量、用户态数据(对于内核程序)、进程间通信(包括管道、消息、共享内存、socket、RPC等,特别需要注意的是设备内部不同单板间通讯也属于进程间通信)、API参数、全局变量。
11
12来自程序外部的数据通常被认为是不可信的,在使用这些数据之前,需要进行合法性校验。
13如果不对这些外部数据进行校验,将可能导致不可预期的安全风险。
14
15注意:不要使用断言检查外部输入数据,断言应该用于防止不正确的程序假设,而不能用在发布版本上检查程序运行过程中发生的错误。
16
17对来自程序外部的数据要校验处理后才能使用。典型场景包括:
18
19**作为数组索引**
20将不可信的数据作为数组索引,可能导致超出数组上限,从而造成非法内存访问。
21**作为内存偏移地址**
22将不可信数据作为指针偏移访问内存,可能造成非法内存访问,并可以造成进一步的危害,如任意地址读/写。
23**作为内存分配的尺寸参数**
24使用0长度分配内存可能造成非法内存访问;未限制分配内存大小会造成过度资源消耗。
25**作为循环条件**
26将不可信数据作为循环限定条件,可能会引发缓冲区溢出、内存越界读/写、死循环等问题。
27**作为除数**
28可能产生除零错误(被零除)。
29**作为命令行参数**
30可能产生命令注入漏洞。
31**作为数据库查询语句的参数**
32可能产生SQL注入漏洞。
33**作为输入/输出格式化字符串**
34可能产生格式化字符串漏洞。
35**作为内存复制长度**
36可能造成缓冲区溢出问题。
37**作为文件路径**
38直接打开不可信路径,可能会导致目录遍历攻击,攻击者操作了无权操作的文件,使得系统被攻击者所控制。
39
40输入校验包括但不局限于:
41
42- API接口参数合法性
43- 校验数据长度
44- 校验数据范围
45- 校验数据类型和格式
46- 校验输入只包含可接受的字符(“白名单”形式),尤其需要注意一些特殊情况下的特殊字符。
47
48**外部数据校验原则**
49
50**1.信任边界**
51
52由于外部数据不可信,因此系统在运行过程中,如果数据传输与处理跨越不同的信任边界,为了防止攻击蔓延,必须对来自信任边界外的其他模块的数据进行合法性校验。
53
54(a)so(或者dll)之间
55
56so或dll作为独立的第三方模块,用于对外导出公共的api函数,供其他模块进行函数调用。so/dll无法确定上层调用者是否传递了合法参数,因此so/dll的公共函数需要检查调用者提供参数的合法性。so/dll应该设计成低耦合、高复用性,尽管有些软件的so/dll当前设计成只在本软件中使用,但仍然应该将不同的so/dll模块视为不同的信任边界。
57
58(b)进程与进程之间
59
60为防止通过高权限进程提权,进程与进程之间的IPC通信(包括单板之间的IPC通信、不同主机间的网络通信),应视为不同信任边界。
61
62(c)应用层进程与操作系统内核
63
64操作系统内核具有比应用层更高的权限,内核向应用层提供的接口,应该将来自应用层的数据作为不可信数据处理。
65
66(d)可信执行环境内外环境
67
68为防止攻击蔓延至可信执行环境,TEE、SGX等对外提供的接口,应该将来自外部的数据作为不可信数据处理。
69
70**2.外部数据校验**
71外部数据进入到本模块后,必须经过合法性校验才能使用。被校验后的合法数据,在本模块内,后续传递到内部其他子函数,不需要重复校验。
72
73**【反例】**
74函数Foo处理外部数据,由于buffer不一定是’\0’结尾, strlen 的返回值 nameLen 有可能超过 len,导致越界读取数据。
75
76```cpp
77void Foo(const unsigned char* buffer, size_t len)
78{
79    // buffer可能为空指针,不保证以'\0'结尾
80    const char* s = reinterpret_cast<const char*>(buffer);
81    size_t nameLen = strlen(s);
82    std::string name(s, nameLen);
83    Foo2(name);
84    ...
85}
86```
87
88**【正例】**
89对外部参数做合法性校验,本例中使用 strnlen 进行字符串长度计算,缓解读越界风险。
90
91```cpp
92void Foo(const unsigned char* buffer, size_t len)
93{
94    // 必须做参数合法性校验
95    if (buffer == nullptr || len == 0 || len >= MAX_BUFFER_LEN) {
96        ... // 错误处理
97    }
98
99    const char* s = reinterpret_cast<const char*>(buffer);
100    size_t nameLen = strnlen(s, len); // 使用strnlen缓解读越界风险
101    if (nameLen == len) {
102        ... // 错误处理
103    }
104    std::string name(s, nameLen);
105    ...
106    Foo2(name);
107    ...
108}
109```
110
111```cpp
112namespace ModuleA {
113// Foo2 为模块内部函数,约定为由调用者保证参数的合法性
114static void Foo2(const std::string& name)
115{
116    ...
117    Bar(name.c_str()); // 调用MODULE_B中的函数
118}
119
120// Foo 为模块的外部接口,需要校验参数的合法性
121void Foo(const unsigned char* buffer, size_t len)
122{
123    // 检查空指针、参数合法范围等
124    if (buffer == nullptr || len <= sizeof(int)) {
125        // 错误处理
126        ...
127    }
128
129    int nameLen = *(reinterpret_cast<const int*>(buffer)); // 从报文中获取name字符串长度
130    // nameLen 是不可信数据,必须检查合法性
131    if (nameLen <= 0 || static_cast<size_t>(nameLen) > len - sizeof(int)) {
132        // 错误处理
133        ...
134    }
135
136    std::string name(reinterpret_cast<const char*>(buffer), nameLen);
137    Foo2(name); // 调用本模块内内部函数
138    ...
139}
140}
141```
142
143以下是使用C语言编写的`MODULE_B`模块中的代码:
144
145```cpp
146// Bar 为 MODULE_B 模块的公共函数,
147// 其约定为,如果参数name不为nullptr,那么必须是一个具有’\0’结尾的合法字符串并且长度大于0
148void Bar(const char* name)
149{
150    // 必须做参数合法性校验
151    if (name == nullptr || name[0] == '\0') {
152        // 错误处理
153        ...
154    }
155    size_t nameLen = strlen(name);  // 不需要使用strnlen
156    ...
157}
158```
159
160对于模块A来说, buffer 是外部不可信输入,必须做严格的校验,从 buffer 解析出来的 name,在解析过程中进行了合法性校验,在模块A内部属于合法数据,作为参数传递给内部子函数时不需要再做合法性校验(如果要继续对 name 内容进行解析,那么仍然必须对 name 内容进行校验)。
161如果模块A中的 name 继续跨越信任面传递给其他模块(在本例中是直接调用模块B的公共函数,也可以是通过文件、管道、网络等方式),那么对于B模块来说, name 属于不可信数据,必须做合法性校验。
162
163# 类
164
165## 类的成员变量必须显式初始化
166
167**【描述】**
168如果没有对类成员变量显示初始化,会使对象处于一种不确定状态。如果类的成员变量具有默认构造函数,那么可以不需要显式初始化。
169
170**【反例】**
171
172```cpp
173class Message {
174public:
175    void Process()
176    {
177        ...
178    }
179
180private:
181    uint32_t msgId;                    // 不符合:成员变量没有被初始化
182    size_t msgLength;                  // 不符合:成员变量没有被初始化
183    unsigned char* msgBuffer;          // 不符合:成员变量没有被初始化
184    std::string someIdentifier;        // 默认构造函数仅会初始化该成员
185};
186
187Message message;                       // message成员变量没有被完全初始化
188message.Process();                     // 后续使用存在隐患
189```
190
191**【正例】**
192一种做法是在类成员变量声明时显示初始化。
193
194```cpp
195class Message {
196public:
197    void Process()
198    {
199        ...
200    }
201
202private:
203    uint32_t msgId{0};
204    size_t msgLength{0};
205    unsigned char* msgBuffer{nullptr};
206    std::string someIdentifier;        // 具有默认构造函数,不需要显式初始化
207};
208```
209
210另一种做法是使用构造函数初始化列表初始化。
211
212```cpp
213class Message {
214public:
215    Message() : msgId(0), msgLength(0), msgBuffer(nullptr) {}
216    void Process()
217    {
218        ...
219    }
220
221private:
222    uint32_t msgId;
223    size_t msgLength;
224    unsigned char* msgBuffer;
225    std::string someIdentifier;        // 具有默认构造函数,不需要显式初始化
226};
227```
228
229## 明确需要实现哪些特殊成员函数
230
231**【描述】**
232**三之法则(Rule of three):**
233若某个类需要用户定义的析构函数、用户定义的拷贝构造函或拷贝赋值操作符,则它基本三者全部都需要。
234
235```cpp
236class Foo {
237public:
238    Foo(const char* buffer, size_t size) { Init(buffer, size); }
239    Foo(const Foo& other) { Init(other.buf, other.size); }
240
241    Foo& operator=(const Foo& other)
242    {
243        Foo tmp(other);
244        Swap(tmp);
245        return *this;
246    }
247
248    ~Foo() { delete[] buf; }
249
250    void Swap(Foo& other) noexcept
251    {
252        using std::swap;
253        swap(buf, other.buf);
254        swap(size, other.size);
255    }
256
257private:
258    void Init(const char* buffer, size_t size)
259    {
260        this->buf = new char[size];
261        memcpy(this->buf, buffer, size);
262        this->size = size;
263    }
264
265    char* buf;
266    size_t size;
267};
268```
269
270如果类对某种资源进行管理,而资源句柄是非类类型的对象(裸指针、文件描述符等),则这些隐式定义的成员函数通常都不正确,其析构函数不做任何事,而拷贝构造函数/拷贝赋值操作符则进行“浅拷贝”。
271
272通过可复制句柄来管理不可复制资源的类,可能必须将其拷贝赋值和拷贝构造函数声明为私有的并且不提供其定义,或将它们定义为delete的。
273
274**五之法则(Rule of five):**
275如果定义了析构函数、拷贝构造函数或拷贝赋值操作符,会阻止移动构造函数和移动赋值操作符的隐式定义,所以任何想要移动语义的类必须声明全部五个特殊成员函数。
276
277```cpp
278class Foo {
279public:
280    Foo(const char* buffer, size_t size) { Init(buffer, size); }
281    Foo(const Foo& other) { Init(other.buf, other.size); }
282
283    Foo& operator=(const Foo& other)
284    {
285        Foo tmp(other);
286        Swap(tmp);
287        return *this;
288    }
289
290    Foo(Foo&& other) noexcept : buf(std::move(other.buf)), size(std::move(other.size))
291    {
292        other.buf = nullptr;
293        other.size = 0;
294    }
295
296    Foo& operator=(Foo&& other) noexcept
297    {
298        Foo tmp(std::move(other));
299        Swap(tmp);
300        return *this;
301    }
302
303    ~Foo() { delete[] buf; }
304
305    void Swap(Foo& other) noexcept
306    {
307        using std::swap;
308        swap(buf, other.buf);
309        swap(size, other.size);
310    }
311
312private:
313    void Init(const char* buffer, size_t size)
314    {
315        this->buf = new char[size];
316        memcpy(this->buf, buffer, size);
317        this->size = size;
318    }
319
320    char* buf;
321    size_t size;
322};
323```
324
325但是如果不提供移动构造函数和移动赋值操作符通常不会发生错误,但会导致失去优化机会。
326
327**零之法则(Rule of zero):**
328如果类不需要专门处理资源的所有权,那么就不应该有自定义的析构函数、拷贝/移动构造函数或拷贝/移动赋值操作符。
329
330```cpp
331class Foo {
332public:
333    Foo(const std::string& text) : text(text) {}
334
335private:
336    std::string text;
337};
338```
339
340只要声明了拷贝构造函数、拷贝赋值操作符或析构函数,编译器将不会隐式生成移动构造函数和移动赋值操作符,导致该类的移动操作都变成了代价更高的复制操作。
341只要声明了移动构造函数或移动赋值操作符,编译器会将隐式生成的拷贝构造函数或拷贝赋值操作符定义为delete的,导致改类只能被移动、不能被复制。
342因此,只要声明了其中的任何一个函数,就应当声明其他全部函数,避免出现非预期的结果。
343
344类似地,如果基类需要定义public的虚析构函数,那么需要显示定义全部相关的特殊成员函数:
345
346```cpp
347class Base {
348public:
349    ...
350    Base(const Base&) = default;
351    Base& operator=(const Base&) = default;
352    Base(Base&&) = default;
353    Base& operator=(Base&&) = default;
354    virtual ~Base() = default;
355    ...
356};
357```
358
359但是,如果基类声明了拷贝构造/拷贝赋值操作符,可能会发生切片,所以经常会将基类中的拷贝构造/拷贝赋值操作符显式定义为delete, 并且同时将其他的特殊成员函数也显式定义为delete:
360
361```cpp
362class Base {
363public:
364    ...
365    Base(const Base&) = delete;
366    Base& operator=(const Base&) = delete;
367    Base(Base&&) = delete;
368    Base& operator=(Base&&) = delete;
369    virtual ~Base() = default;
370    ...
371};
372```
373
374## 基类中的拷贝构造函数、拷贝赋值操作符、移动构造函数、移动赋值操作符必须为非public函数或者为delete函数
375
376**【描述】**
377如果把一个派生类对象直接赋值给基类对象,会发生切片,只拷贝或者移动了基类部分,损害了多态行为。
378
379**【反例】**
380如下代码中,基类的拷贝构造函数和拷贝赋值操作符为default,如果派生类对象赋值给基类对象时会发生切片。
381可以将此例中的拷贝构造函数和拷贝赋值操作符声明为delete,编译器可检查出此类赋值行为。
382
383```cpp
384class Base {
385public:
386    Base() = default;
387    Base(const Base&) = default;
388    Base& operator=(const Base&) = default;
389    ...
390    virtual void Fun() { std::cout << "Base" << std::endl; }
391};
392
393class Derived : public Base {
394    ...
395    void Fun() override { std::cout << "Derived" << std::endl; }
396};
397
398void Foo(const Base& base)
399{
400    Base other = base;    // 不符合:发生切片
401    other.Fun();          // 调用的是Base类的Fun函数
402}
403Derived d;
404Foo(d);
405```
406
407## 在移动构造函数和移动赋值操作符中必须将源对象的资源正确重置
408
409**【描述】**
410移动构造函数和移动赋值操作符将资源的所有权从一个对象移动到另外一个资源。一旦资源被移动,则应将源对象的资源正确重置。这样可以防止源对象在析构函数中释放了被移动的资源。
411
412在被移动的对象中允许保留部分非资源相关数据,但必须保证被移动的对象处于可被正常析构的状态。
413因此,当一个对象被move以后,除非该对象处于明确指定的状态,否则不要依赖已move对象的值,否则可能产生非预期行为。
414
415**【反例】**
416
417```cpp
418class Foo {
419public:
420    ...
421    Foo(Foo&& foo) noexcept : data(foo.data)
422    {
423    }
424
425    Foo& operator=(Foo&& foo)
426    {
427        data = foo.data;
428        return *this;
429    }
430
431    ~Foo()
432    {
433        delete[] data;
434    }
435
436private:
437    char* data = nullptr;
438};
439```
440
441上述Foo的移动构造函数和移动赋值操作符没有正确将源对象的资源重置,源对象析构的时候会将资源释放,导致新创建的对象中接管的资源成为无效资源。
442
443**【正例】**
444
445```cpp
446class Foo {
447public:
448    ...
449    Foo(Foo&& foo) noexcept : data(foo.data)
450    {
451        foo.data = nullptr;
452    }
453
454    Foo& operator=(Foo&& foo)
455    {
456        if (this == &foo) {
457            return *this;
458        }
459        delete[] data;
460        data = foo.data;
461        foo.data = nullptr;
462        return *this;
463    }
464
465    ~Foo()
466    {
467        delete[] data;
468    }
469
470private:
471    char* data = nullptr;
472};
473```
474
475此外,不要依赖已经被move对象的值。
476某些标准库std::string的实现可能对短字节做优化,在实现移动语义时可能不会修改被移动字符串的内容,导致如下代码输出不一定是预期的b, 有可能输出为ab,存在兼容性问题。
477
478```cpp
479std::string str{"a"};
480std::string other = std::move(str);
481
482str.append(1, 'b');
483std::cout << str << std::endl;
484```
485
486## 通过基类指针释放派生类时,必须将基类中析构函数声明为虚函数
487
488**【描述】**
489只有基类析构函数是虚函数时,才能保证通过多态调用的时候调用到派生类的析构函数。
490如果没有将基类的析构函数声明为虚函数,当通过基类指针释放派生类时,只会调用基类的析构函数,不会调用派生类的析构函数,导致内存泄漏。
491
492**【反例】**
493没有将基类的析构函数声明为虚函数,导致了内存泄漏。
494
495```cpp
496class Base {
497public:
498    Base() = default;
499    ~Base() { std::cout << "~Base" << std::endl; }
500    virtual std::string GetVersion() = 0;
501};
502class Derived : public Base {
503public:
504    Derived()
505    {
506        const size_t numberCount = 100;
507        numbers = new int[numberCount];
508    }
509
510    ~Derived()
511    {
512        delete[] numbers;
513        std::cout << "~Derived" << std::endl;
514    }
515
516    std::string GetVersion()
517    {
518        return std::string("hello!");
519    }
520
521private:
522    int* numbers;
523};
524void Foo()
525{
526    Base* base = new Derived();
527    delete base;                // 调用的是 Base 的析构函数,造成资源泄漏
528}
529```
530
531## 对象赋值或初始化避免切片操作
532
533**【描述】**
534
535将派生类对象按值赋值给基类对象时会发生切片,损害了多态行为。
536
537如果确实需要将对象切片处理,建议定义一个显式操作完成这个功能,以避免理解错误,增加可维护性。
538
539**【反例】**
540
541```cpp
542class Base {
543     virtual void Fun();
544};
545
546class Derived : public Base {
547    ...
548};
549void Foo(const Base& base)
550{
551    Base other = base;        // 不符合:发生切片
552    other.Fun();              // 调用的是Base类的Fun函数
553}
554Derived d;
555Base b{d};                    // 不符合:仅构造了Base部分
556b = d;                        // 不符合:仅赋值Base部分
557
558Foo(d);
559```
560
561# 表达式与语句
562
563## 确保对象在使用之前已被初始化
564
565**【描述】**
566本条款中的“初始化”指的是通过定义时显示初始化、默认构造初始化、赋值等方式使对象拥有期望的值。
567读取一个未初始化的值时,程序可能产生未定义行为,因此需要确保对象在使用之前已被初始化。
568
569**【反例】**
570
571```cpp
572void Bar(int data);
573...
574void Foo()
575{
576    int data;
577    Bar(data); // 不符合:未初始化就使用
578    ...
579}
580```
581
582如果有不同分支,要确保所有分支都得到初始化后才能使用。
583
584```cpp
585void Bar(int data);
586...
587void Foo(int condition)
588{
589    int data;
590    if (condition > 0) {
591        data = CUSTOMIZED_SIZE;
592    }
593    Bar(data);      // 不符合:部分分支该值未初始化
594    ...
595}
596```
597
598**【正例】**
599
600```cpp
601void Bar(int data);
602...
603void Foo()
604{
605    int data{0};    // 符合:显示初始化
606    Bar(data);
607    ...
608}
609void InitData(int& data);
610...
611void Foo()
612{
613    int data;
614    InitData(data); // 符合:通过函数初始化
615    ...
616}
617std::string data;   // 符合:默认构造函数初始化
618...
619```
620
621## 避免使用reinterpret_cast
622
623**【描述】**
624`reinterpret_cast`用于转换不相关类型。尝试用`reinterpret_cast`将一种类型强制转换另一种类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。
625
626## 避免使用const_cast
627
628**【描述】**
629`const_cast`用于移除对象的`const`和`volatile`性质。
630
631使用const_cast转换后的指针或者引用来修改const对象或volatile对象,程序会产生未定义行为。
632
633**【反例】**
634
635```cpp
636const int i = 1024;
637int* p = const_cast<int*>(&i);
638*p = 2048;                              // 未定义行为
639class Foo {
640public:
641    void SetValue(int v) { value = v; }
642
643private:
644    int value{0};
645};
646
647int main()
648{
649    const Foo foo;
650    Foo* p = const_cast<Foo*>(&foo);
651    p->SetValue(2);                     // 未定义行为
652    return 0;
653}
654```
655
656## 确保有符号整数运算不溢出
657
658**【描述】**
659在C++标准中,有符号整数溢出会使程序产生未定义行为。
660因此,不同的实现可以自由处理有符号整数溢出。例如:在将有符号整数类型定义为模数的实现中,编译器可以不检测整数溢出。
661
662使用溢出后的数值可能导致程序缓冲区读写越界等风险。出于安全考虑,对外部数据中的有符号整数值在如下场景中使用时,需要确保运算不会导致溢出:
663
664- 指针运算的整数操作数(指针偏移值)
665- 数组索引
666- 变长数组的长度(及长度运算表达式)
667- 内存复制长度
668- 内存分配函数的参数
669- 循环判断条件
670
671在精度低于int的整数类型上进行运算时,需要考虑整数提升。程序员还需要掌握整数转换规则,包括隐式转换规则,以便设计安全的算术运算。
672
673**【反例】**
674如下代码示例中,参与减法运算的整数是外部数据,在使用前未做校验,可能出现整数溢出,进而造成后续的内存复制操作出现缓冲区溢出。
675
676```cpp
677unsigned char* content = ... // 指向报文头的指针
678size_t contentSize = ...     // 缓冲区的总长度
679int totalLen = ...           // 报文总长度
680int skipLen = ...            // 从消息中解析出来的需要忽略的数据长度
681
682std::vector<unsigned char> dest;
683
684// 用 totalLen - skipLen 计算剩余数据长度,可能出现整数溢出
685std::copy_n(&content[skipLen], totalLen - skipLen, std::back_inserter(dest));
686...
687```
688
689**【正例】**
690如下代码示例中,重构为使用`size_t`类型的变量表示数据长度,并校验外部数据长度是否在合法范围内。
691
692```cpp
693unsigned char* content = ... //指向报文头的指针
694size_t contentSize = ...     // 缓冲区的总长度
695size_t totalLen = ...        // 报文总长度
696size_t skipLen = ...         // 从消息中解析出来的需要忽略的数据长度
697
698if (skipLen >= totalLen || totalLen > contentSize) {
699    ... // 错误处理
700}
701
702std::vector<unsigned char> dest;
703std::copy_n(&content[skipLen], totalLen - skipLen, std::back_inserter(dest));
704...
705```
706
707**【反例】**
708如下代码示例中,对来自外部数据的数值范围做了校验,但是由于second是`int`类型,而校验条件中错误的使用了`std::numeric_limits<unsigned long>::max()`进行限制,导致整数溢出。
709
710```cpp
711int second = ... // 来自外部数据
712
713 // 错误的使用了unsigned long的取值范围做上限校验
714if (second < 0 || second > (std::numeric_limits<unsigned long>::max() / 1000)) {
715    return -1;
716}
717int millisecond = second * 1000; // 可能出现整数溢出
718...
719```
720
721**【正例】**
722一种改进方案是将second的类型修改为`unsigned long`类型,这种方案适用于修改了变量类型更符合业务逻辑的场景。
723
724```cpp
725unsigned long second = ... // 将类型重构为 unsigned long 类型
726
727if (second > (std::numeric_limits<unsigned long>::max() / 1000)) {
728    return -1;
729}
730int millisecond = second * 1000;
731...
732```
733
734另一种改进方案是将数值上限修改为`std::numeric_limits<int>::max()`。
735
736```cpp
737int second = ... // 来自外部数据
738
739if (second < 0 || second > (std::numeric_limits<int>::max() / 1000)) {
740    return -1;
741}
742int millisecond = second * 1000;
743```
744
745**【影响】**
746整数溢出可能导致程序缓冲区溢出以及执行任意代码。
747
748## 确保无符号整数运算不回绕
749
750**【描述】**
751无符号整数的算术运算结果可能会发生整数回绕,使用回绕后的数值其可能导致程序缓冲区读写越界等风险。
752出于安全考虑,对外部数据中的无符号整数值在如下场景中使用时,需要确保运算不会导致回绕:
753
754- 指针偏移值(指针算术运算的整数操作数)
755- 数组索引值
756- 内存拷贝的长度
757- 内存分配函数的参数
758- 循环判断条件
759
760**【反例】**
761如下代码示例中,校验下一个子报文的长度加上已处理报文的长度是否超过了整体报文的最大长度,在校验条件中的加法运算可能会出现整数回绕,造成绕过该校验的问题。
762
763```cpp
764size_t totalLen = ...              // 报文的总长度
765size_t readLen = 0;                // 记录已经处理报文的长度
766...
767size_t pktLen = ParsePktLen();     // 从网络报文中解析出来的下一个子报文的长度
768if (readLen + pktLen > totalLen) { // 可能出现整数回绕
769  ... // 错误处理
770}
771...
772readLen += pktLen;
773...
774```
775
776**【正例】**
777由于readLen变量记录的是已经处理报文的长度,必然会小于totalLen,因此将代码中的加法运算修改为减法运算,不会导致条件绕过。
778
779```cpp
780size_t totalLen = ... // 报文的总长度
781size_t readLen = 0;   // 记录已经处理报文的长度
782...
783size_t pktLen = ParsePktLen(); // 来自网络报文
784if (pktLen > totalLen - readLen) {
785  ... // 错误处理
786}
787...
788readLen += pktLen;
789...
790```
791
792**【反例】**
793如下代码示例中,校验len合法范围的运算可能会出现整数回绕,导致条件绕过。
794
795```cpp
796size_t len = ... // 来自用户态输入
797
798if (SCTP_SIZE_MAX - len < sizeof(SctpAuthBytes)) { // 减法操作可能出现整数回绕
799    ... // 错误处理
800}
801... = kmalloc(sizeof(SctpAuthBytes) + len, gfp);   // 可能出现整数回绕
802...
803```
804
805**【正例】**
806如下代码示例中,调整减法运算的位置(需要确保在编译期间减法表达式的值不回绕),避免整数回绕问题。
807
808```cpp
809size_t len = ... // 来自用户态输入
810
811if (len > SCTP_SIZE_MAX - sizeof(SctpAuthBytes)) { // 确保在编译期间减法表达式的值不翻转
812    ... // 错误处理
813}
814... = kmalloc(sizeof(SctpAuthBytes) + len, gfp);
815...
816```
817
818**【例外】**
819为正确执行程序,必要时无符号整数可能表现出模态(回绕)。建议将变量声明明确注释为支持模数行为,并且对该整数的每个操作也应明确注释为支持模数行为。
820
821**【影响】**
822整数回绕可能导致程序缓冲区溢出以及执行任意代码。
823
824## 确保除法和余数运算不会导致除零错误(被零除)
825
826**【描述】**
827整数的除法运算或取余运算的除数为0会导致程序产生未定义的行为。如果涉及到除法或者取余运算,必须确保除数不为0。
828
829在二进制浮点数算数标准ISO/IEEE Std 754-1985中规定了浮点数被零除的行为及结果,但是仍然取决于程序所运行的软硬件环境是否遵循该标准。
830因此,在做浮点数被零除的运算前,应确保软硬件环境已遵循二进制浮点数算数标准,否则仍然存在未定义行为。
831
832**【反例】**
833
834```c
835size_t a = ReadSize();  // 来自外部数据
836size_t b = 1000 / a;    // 不符合:a可能是0
837size_t c = 1000 % a;    // 不符合:a可能是0
838...
839```
840
841**【正例】**
842如下代码示例中,添加a是否为0的校验,防止除零错误。
843
844```c
845size_t a = ReadSize();  // 来自外部数据
846if (a == 0) {
847    ... // 错误处理
848}
849size_t b = 1000 / a;    // 符合:确保a不为0
850size_t c = 1000 % a;    // 符合:确保a不为0
851...
852```
853
854**【影响】**
855除零错误可能导致拒绝服务。
856
857## 只能对无符号整数进行位运算
858
859**【描述】**
860对有符号整数进行位运算时可能产生未定义行为,本条款要求只能对无符号整数进行位运算,避免出现未定义行为。
861此外,对精度低于int类型的无符号整数进行位运算时,编译器会进行整数提升,再对提升后的整数进行位运算,因此要特别注意对于这类无符号整数的位运算,避免出现非预期的结果。
862本条款涉及的位操作符包括:
863
864- `~`(求反)
865- `&`(与)
866- `|`(或)
867- `^`(异或)
868- `>>`(右移位)
869- `<<`(左移位)
870- `&=`
871- `^=`
872- `|=`
873- `>>=`
874- `<<=`
875
876在C++20中有符号整数的移位操作具有良好的定义,可以对有符号整数进行移位运算。
877
878**【反例】**
879在C++20之前,如下代码中的右移操作`data >> 24`可以实现为算术(有符号)移位或逻辑(无符号)移位;在左移操作`value << data`中,如果value为负数或者左移后的结果超出其整数提升后类型的表示范围,会导致程序产生未定义行为。
880
881```cpp
882int32_t data = ReadByte();
883int32_t value = data >> 24;   // 对有符号整数进行右移运算,其结果是实现定义的
884
885... // 检查 data 的合法范围
886
887int32_t mask = value << data; // 对有符号整数进行左移运算,程序可能产生未定义行为
888```
889
890**【正例】**
891
892```cpp
893uint32_t data = static_cast<uint32_t>(ReadByte());
894uint32_t value = data >> 24;  // 只对无符号整数进行位运算
895
896... // 检查 data 的合法范围
897
898uint32_t mask  = value << data;
899```
900
901对于精度低于`int`的无符号整数进行位运算,由于整数提升,其结果可能是非预期的,应将操作结果立即转换为期望的类型, 避免因整数提升而导致非预期结果。
902
903**【反例】**
904
905```cpp
906uint8_t mask = 1;
907uint8_t value = (~mask) >> 4; // 不符合:~运算的结果会包含高位数据,可能不符合预期
908```
909
910**【正例】**
911
912```cpp
913uint8_t mask = 1;
914uint8_t value = (static_cast<uint8_t>(~mask)) >> 4; // 符合:~运算后立即转换为期望的类型
915```
916
917**【例外】**
918
919- 作为位标志使用的有符号整数常量或枚举值,可以作为 & 和 | 操作符的操作数。
920
921```cpp
922int fd = open(fileName, O_CREAT | O_EXCL, S_IRWXU | S_IRUSR);
923```
924
925- 一个在编译时就可以确定的有符号正整数,可以作为移位操作符的右操作数。
926
927```cpp
928constexpr int SHIFT_BITS = 3;
929...
930uint32_t id = ...;
931uint32_t type = id >> SHIFT_BITS;
932```
933
934# 资源管理
935
936## 外部数据作为数组索引或者内存操作长度时,需要校验其合法性
937
938**【描述】**
939外部数据作为数组索引对内存进行访问时,必须对数据的大小进行严格的校验,确保数组索引在有效范围内,否则会导致严重的错误。
940将数据复制到容量不足以容纳该数据的内存中会导致缓冲区溢出。为了防止此类错误,必须根据目标容量的大小限制被复制的数据大小,或者必须确保目标容量足够大以容纳要复制的数据。
941
942**【反例】**
943如下代码示例中,SetDevId()函数存在差一错误,当 index 等于 `DEV_NUM` 时,恰好越界写一个元素。
944
945```cpp
946struct Dev {
947    int id;
948    char name[MAX_NAME_LEN];
949};
950
951static Dev devs[DEV_NUM];
952
953int SetDevId(size_t index, int id)
954{
955    if (index > DEV_NUM) {         // 存在差一错误
956        ... // 错误处理
957    }
958
959    devs[index].id = id;
960    return 0;
961}
962```
963
964**【正例】**
965如下代码示例中,修改校验索引的条件,避免差一错误。
966
967```cpp
968struct Dev {
969    int id;
970    char name[MAX_NAME_LEN];
971};
972
973static Dev devs[DEV_NUM];
974
975int SetDevId(size_t index, int id)
976{
977    if (index >= DEV_NUM) {
978        ... // 错误处理
979    }
980    devs[index].id = id;
981    return 0;
982}
983```
984
985**【反例】**
986外部输入的数据不一定会直接作为内存复制长度使用,还可能会间接参与内存复制操作。
987如下代码示例中,inputTable.count来自设备外部报文,虽然没有直接作为内存复制长度使用,而是作为for循环体的上限使用,间接参与了内存复制操作。由于没有校验其大小,可造成缓冲区溢出:
988
989```cpp
990struct ValueTable {
991    size_t count;
992    int val[MAX_NUMBERS];
993};
994
995void ValueTableDup(const ValueTable& inputTable)
996{
997    ValueTable outputTable = {0, {0}};
998    ...
999    for (size_t i = 0; i < inputTable.count; i++) {
1000        outputTable.val[i] = inputTable.val[i];
1001    }
1002    ...
1003}
1004```
1005
1006**【正例】**
1007如下代码示例中,对inputTable.count做了校验。
1008
1009```cpp
1010struct ValueTable {
1011    size_t count;
1012    int val[MAX_NUMBERS];
1013};
1014
1015void ValueTableDup(const ValueTable& inputTable)
1016{
1017    ValueTable outputTable = {0, {0}};
1018    ...
1019    // 根据业务场景,对来自外部报文的循环长度inputTable.count
1020    // 与outputTable.val数组大小做校验,避免造成缓冲区溢出
1021    if (inputTable->count >
1022        sizeof(outputTable.val) / sizeof(outputTable.val[0])) {
1023        ... // 错误处理
1024    }
1025    for (size_t i = 0; i < inputTable.count; i++) {
1026        outputTable.val[i] = inputTable.val[i];
1027    }
1028    ...
1029}
1030```
1031
1032**【影响】**
1033如果复制数据的长度是外部可控的,则复制数据的过程中可能出现缓冲区溢出,在某些情况下可以造成任意代码执行漏洞。
1034
1035## 内存申请前,必须对申请内存大小进行合法性校验
1036
1037**【描述】**
1038当申请内存大小由程序外部输入时,内存申请前,要求对申请内存大小进行合法性校验,防止申请0长度内存,或者过多地、非法地申请内存。
1039因为内存的资源是有限的,是可以被耗尽的。当申请内存的数值过大(可能一次就申请了非常大的超预期的内存;也可能循环中多次申请内存),很可能会造成非预期的资源耗尽。
1040大小不正确的参数、不当的范围检查、整数溢出或者截断都可能造成实际分配的缓冲区不符合预期。如果申请内存受攻击者控制,还可能会发生缓冲区溢出等安全问题。
1041
1042**【反例】**
1043如下代码示例中,将动态分配size大小的内存。但是未对size做合法性校验。
1044
1045```c
1046// 这里的size在传入DoSomething()函数之前还未做过合法性校验
1047int DoSomething(size_t size)
1048{
1049    ...
1050    char* buffer = new char[size]; // 本函数内,size使用前未做校验
1051    ...
1052    delete[] buffer;
1053}
1054```
1055
1056**【正例】**
1057如下代码示例中,动态分配size大小的内存前,进行了符合程序需要的合法性校验。
1058
1059```c
1060// 这里的size在传入DoSomething()函数之前还未做过合法性校验
1061int DoSomething(size_t size)
1062{
1063    // 本函数内,对size做合法性校验,FOO_MAX_LEN被定义为符合程序设计预期的最大内存空间
1064    if (size == 0 || size > FOO_MAX_LEN) {
1065        ... // 错误处理
1066    }
1067    char* buffer = new char[size];
1068    ...
1069    delete[] buffer;
1070}
1071```
1072
1073**【影响】**
1074如果申请内存的大小是外部可控的,可能导致资源耗尽,造成拒绝服务。
1075
1076## 在传递数组参数时,不应单独传递指针
1077
1078**【描述】**
1079当函数参数类型为数组(不是数组的引用)或者指针时,若调用者传入数组,则在参数传递时数组会退化为指针,其数组长度信息会丢失,容易引发越界读写等问题。
1080如果函数只接收固定长度的数组为参数,可以定义参数类型为数组引用或者`std::array`。
1081如果函数接受的是不带长度的指针,那么应该把长度作为另外一个参数也传递进去。
1082
1083**【反例】**
1084
1085```cpp
1086constexpr int MAX_LEN = 1024;
1087constexpr int SIZE = 10;
1088
1089void UseArr(int arr[])
1090{
1091    for (int i = 0; i < MAX_LEN; i++) {
1092        std::cout << arr[i] << std::endl;
1093    }
1094}
1095
1096void Test()
1097{
1098    int arr[SIZE] = {0};
1099    UseArr(arr);
1100}
1101```
1102
1103**【正例】**
1104
1105可以把指针和长度合起来做成一个类型,方便使用。例如:类似下面的简单封装:
1106
1107```cpp
1108template <typename T>
1109class Slice {
1110public:
1111    template <size_t N>
1112    Slice(T (&arr)[N]) : data(arr), len(N) {}
1113
1114    template <size_t N>
1115    Slice(std::array<T, N> arr) : data(arr.data()), len(N) {}
1116
1117    Slice(T* arr, size_t n) : data(arr), len(n) {}
1118    ...
1119
1120private:
1121    T* data;
1122    size_t len;
1123};
1124
1125void UseArr(Slice<int> arr)
1126{
1127    for (int i = 0; i < arr.size(); i++) {
1128        std::cout << arr[i] << std::endl;
1129    }
1130}
1131
1132constexpr int SIZE = 10;
1133
1134void Test()
1135{
1136    int arr[SIZE] = {0};
1137    Slice<int> s{arr};
1138    UseArr(s);
1139}
1140```
1141
1142如果项目允许的话,推荐使用成熟的库来做这个事情,例如C++20中的`std::span`类型。
1143
1144在不使用这些工具类的情况下,可以把指针和长度作为两个参数传递。
1145
1146```cpp
1147void UseArr(int arr[], size_t len)
1148{
1149    for (int i = 0; i < len; i++) {
1150        std::cout << arr[i] << std::endl;
1151    }
1152}
1153
1154constexpr int SIZE = 10;
1155
1156void Test()
1157{
1158    int arr[SIZE] = {0};
1159    UseArr(arr, sizeof(arr));
1160}
1161```
1162
1163## 当lambda会逃逸出函数外面时,禁止按引用捕获局部变量
1164
1165**【描述】**
1166如果一个 lambda 不止在局部范围内使用,禁止按引用捕获局部变量,比如它被传递到了函数的外部,或者被传递给了其他线程的时候。lambda按引用捕获就是把局部对象的引用存储起来。如果 lambda 的生命周期会超过局部变量生命周期,则可能导致内存不安全。
1167
1168**【反例】**
1169
1170```cpp
1171void Foo()
1172{
1173    int local = 0;
1174    // 按引用捕获 local,当函数返回后,local 不再存在,因此 Process() 的行为未定义
1175    threadPool.QueueWork([&] { Process(local); });
1176}
1177```
1178
1179**【正例】**
1180
1181```cpp
1182void Foo()
1183{
1184    int local = 0;
1185    // 按值捕获 local, 在Process() 调用过程中,local 总是有效的
1186    threadPool.QueueWork([local] { Process(local); });
1187}
1188```
1189
1190## 指向资源句柄或描述符的变量,在资源释放后立即赋予新值
1191
1192**【描述】**
1193指向资源句柄或描述符的变量包括指针、文件描述符、socket描述符以及其他指向资源的变量。
1194以指针为例,当指针成功申请了一段内存之后,在这段内存释放以后,如果其指针未立即设置为nullptr,也未分配一个新的对象,那这个指针就是一个悬空指针。
1195如果再对悬空指针操作,可能会发生重复释放或访问已释放内存的问题,造成安全漏洞。
1196消减该漏洞的有效方法是将释放后的指针立即设置为一个确定的新值,例如设置为nullptr。对于全局性的资源句柄或描述符,在资源释放后,应该马上设置新值,以避免使用其已释放的无效值;对于只在单个函数内使用的资源句柄或描述符,应确保资源释放后其无效值不被再次使用。
1197
1198**【反例】**
1199如下代码示例中,根据消息类型处理消息,处理完后释放掉body指向的内存,但是释放后未将指针设置为nullptr。如果还有其他函数再次处理该消息结构体时,可能出现重复释放内存或访问已释放内存的问题。
1200
1201```c
1202int Fun()
1203{
1204    SomeStruct *msg = nullptr;
1205
1206    ... // 使用new分配msg、msg->body的内存空间并初始化msg
1207
1208    if (msg->type == MESSAGE_A) {
1209        ...
1210        delete msg->body;         // 不符合:释放内存后,未置空
1211    }
1212
1213    ...
1214
1215    // 将msg存入全局队列,后续可能使用已释放的body成员
1216    if (!InsertMsgToQueue(msg)) {
1217        delete msg->body;         // 可能再次释放了body的内存
1218        delete msg;
1219        return -1;
1220    }
1221    return 0;
1222}
1223```
1224
1225**【正例】**
1226如下代码示例中,立即对释放后的指针设置为nullptr,避免重复释放指针。
1227
1228```c
1229int Fun()
1230{
1231    SomeStruct *msg = nullptr;
1232
1233    ... // 使用new分配msg、msg->body的内存空间并初始化msg
1234
1235    if (msg->type == MESSAGE_A) {
1236        ...
1237        delete msg->body;
1238        msg->body = nullptr;
1239    }
1240
1241    ...
1242
1243    // 将msg存入全局队列
1244    if (!InsertMsgToQueue(msg)) {
1245        delete msg->body;         // 马上离开作用域,不必赋值 nullptr
1246        delete msg;               // 马上离开作用域,不必赋值 nullptr
1247        return -1;
1248    }
1249    return 0;
1250}
1251```
1252
1253默认的内存释放函数针对空指针不执行任何动作。
1254
1255**【反例】**
1256如下代码示例中文件描述符关闭后未赋新值。
1257
1258```c
1259SOCKET s = INVALID_SOCKET;
1260int fd = -1;
1261...
1262closesocket(s);
1263...
1264close(fd);
1265...
1266```
1267
1268**【正例】**
1269如下代码示例中,在资源释放后,对应的变量应该立即赋予新值。
1270
1271```c
1272SOCKET s = INVALID_SOCKET;
1273int fd = -1;
1274...
1275closesocket(s);
1276s = INVALID_SOCKET;
1277...
1278close(fd);
1279fd = -1;
1280...
1281```
1282
1283**【影响】**
1284再次使用已经释放的内存,或者再次释放已经释放的内存,或其他使用已释放资源的行为,可能导致拒绝服务或执行任意代码。
1285
1286## new和delete配对使用,new[]和delete[]配对使用
1287
1288**【描述】**
1289使用 new 操作符创造的对象,只能使用 delete 操作符来销毁。
1290使用 new[] 创造的对象数组只能由 delete[] 操作符来销毁。
1291
1292**【反例】**
1293
1294```cpp
1295class C {
1296public:
1297    C(size_t len) : arr(new int[len]) {}
1298    ~C()
1299    {
1300        delete arr; // 此处应该是 delete[] arr;
1301    }
1302
1303private:
1304    int* arr;
1305};
1306```
1307
1308**【正例】**
1309
1310```cpp
1311class C {
1312public:
1313    C(size_t len) : arr(new int[len]) {}
1314    ~C() { delete[] arr; }
1315
1316private:
1317    int* arr;
1318};
1319```
1320
1321## 自定义new/delete操作符需要配对定义,且行为与被替换的操作符一致
1322
1323**【描述】**
1324自定义操作符的时候,new 和 delete 要配对定义,new[] 和 delete[] 要配对定义。
1325自定义 new/delete 操作符的行为要与被替换的 new/delete 的行为一致。
1326
1327**【反例】**
1328
1329```cpp
1330// 如果自定义了 operator new,必须同时自定义对应的 operator delete
1331struct S {
1332    static void* operator new(size_t sz)
1333    {
1334        ... // 自定义操作
1335        return ::operator new(sz);
1336    }
1337};
1338```
1339
1340**【正例】**
1341
1342```cpp
1343struct S {
1344    static void* operator new(size_t sz)
1345    {
1346        ... // 自定义操作
1347        return ::operator new(sz);
1348    }
1349    static void operator delete(void* ptr, size_t sz)
1350    {
1351        ... // 自定义操作
1352        ::operator delete(ptr);
1353    }
1354};
1355```
1356
1357默认的 new 操作符在内存分配失败时,会抛出`std::bad_alloc`异常,而使用了`std::nothrow`参数的 new 操作符在内存分配失败时,会返回 nullptr。
1358自定义的 new/delete 操作符要和内置的操作符行为保持一致。
1359
1360**【反例】**
1361
1362```cpp
1363// 在内存管理模块头文件中声明的函数
1364extern void* AllocMemory(size_t size);   // 分配失败返回 nullptr
1365void* operator new(size_t size)
1366{
1367    return AllocMemory(size);
1368}
1369```
1370
1371**【正例】**
1372
1373```cpp
1374// 在内存管理模块头文件中声明的函数
1375extern void* AllocMemory(size_t size);   // 分配失败返回 nullptr
1376void* operator new(size_t size)
1377{
1378    void* ret = AllocMemory(size);
1379    if (ret != nullptr) {
1380        return ret;
1381    }
1382    throw std::bad_alloc();              // 分配失败抛出异常
1383}
1384
1385void* operator new(size_t size, const std::nothrow_t& tag)
1386{
1387    return AllocMemory(size);
1388}
1389```
1390
1391# 错误处理
1392
1393## 抛异常时,抛对象本身,而不是指向对象的指针
1394
1395**【描述】**
1396C++中推荐的抛异常方式是抛对象本身,而不是指向对象的指针。
1397
1398用throw语句抛出异常的时候,会构造一个临时对象,称为“异常对象(exception object)”。这个异常对象的生命周期在C++语言中很明确:异常对象在throw时被构造;在某个捕获它的catch语句以`throw`以外的方式结束(即没有重新抛出)时,或者指向这个异常的`std::exception_ptr`对象被析构时析构。
1399
1400抛出指针,会使回收被抛出对象的责任不明确。捕获异常的地方是否有义务对该指针进行`delete`操作,取决于该对象是如何分配的(例如静态变量,或者用`new`分配),以及这个对象是否被共享了。但是指针类型本身并不能表明这个对象的生命周期以及所有权,也就无法判断是否应该`delete`。如果应该`delete`却没有做,会造成内存泄露;如果不该`delete`却做了,会造成重复释放。
1401
1402**【反例】**
1403
1404不要抛指针。
1405
1406```cpp
1407static SomeException exc1("reason 1");
1408
1409try {
1410    if (SomeFunction()) {
1411        throw &exc1;                         // 不符合:这是静态对象的指针,不应该delete
1412    } else {
1413        throw new SomeException("reason 2"); // 不符合:这是动态分配的,应该delete
1414    }
1415} catch (const SomeException* e) {
1416    delete e;                                // 不符合:这里不能确定是否需要delete
1417}
1418```
1419
1420**【正例】**
1421
1422永远抛异常对象本身。
1423
1424```cpp
1425try {
1426    if (SomeFunction()) {
1427        throw SomeException("reason 1");
1428    } else {
1429        throw SomeException("reason 2");
1430    }
1431} catch (const SomeException& e) {
1432    ...                                      // 符合:这里可以确定不需要delete
1433}
1434```
1435
1436## 禁止从析构函数中抛出异常
1437
1438**【描述】**
1439
1440析构函数默认自带`noexcept`属性,如果析构函数抛出异常,会直接导致`std::terminate`。自C++11起,允许析构函数被标记为`noexcept(false)`,但即便如此,如果析构函数在stack unwinding的过程中被调用(例如另一个异常抛出,导致栈上的局部变量被析构),结果也是`std::terminate`,而析构函数最大的作用就是在不论正常返回还是抛出异常的情况下都能清理局部变量。因此,让析构函数抛出异常一般都是不好的。
1441
1442# 标准库
1443
1444## 禁止从空指针创建std::string
1445
1446**【描述】**
1447将空指针传递给std::string构造函数,会解引用空指针,从而导致程序产生未定义行为。
1448
1449**【反例】**
1450
1451```cpp
1452void Foo()
1453{
1454    const char* path = std::getenv("PATH");
1455    std::string str(path);                  // 错误:这里没有判断getenv的返回值是否为nullptr
1456    std::cout << str << std::endl;
1457}
1458```
1459
1460**【正例】**
1461
1462```cpp
1463void Foo()
1464{
1465    const char* path = std::getenv("PATH");
1466    if (path == nullptr) {
1467        ... // 报告错误
1468        return;
1469    }
1470    std::string str(path);
1471    ...
1472    std::cout << str << std::endl;
1473}
1474void Foo()
1475{
1476    const char* path = std::getenv("PATH");
1477    std::string str(path == nullptr ? path : "");
1478    ... // 判断空字符串
1479    std::cout << str << std::endl;                // 必要时判断空字符串
1480}
1481```
1482
1483## 不要保存std::string类型的c_str和data成员函数返回的指针
1484
1485**【描述】**
1486为保证调用std::string对象的c_str()和data()成员函数返回的引用值结果的有效性,不应保存std::string类型的c_str()和data()的结果,而是在每次需要时直接调用(调用的开销会被编译器内联优化)。否则,当调用此std::string对象的修改方法修改对象后,或超出std::string对象作用域时,之前存储的指针将会失效。使用失效的指针将导致未定义行为。
1487
1488**【反例】**
1489
1490```cpp
1491void Bar(const char*  data)
1492{
1493    ...
1494}
1495
1496void Foo1()
1497{
1498    std::string name{"demo"};
1499    const char* text = name.c_str();          // 表达式结束以后,name的生命周期还在,指针有效
1500
1501    // 如果中间调用了std::string的非const成员函数,导致name被修改,例如operator[], begin()等,
1502    // 可能会导致text的内容不可用,或者不是原来的字符串
1503    name = "test";
1504    name[1] = '2';
1505    ...
1506    Bar(text);                                // 此处text已不再指向合法内存空间
1507}
1508
1509void Foo2()
1510{
1511    std::string name{"demo"};
1512    std::string test{"test"};
1513    const char* text = (name + test).c_str(); // 表达式结束以后,+号产生的临时对象被销毁
1514    ...
1515    Bar(text);                                // 此处text已不再指向合法内存空间
1516}
1517
1518void Foo3(std::string& s)
1519{
1520    const char* data = s.data();
1521    ...
1522    s.replace(0, 3, "***");
1523    ...
1524    Bar(data);                                // 此处text已不再指向合法内存空间
1525}
1526```
1527
1528**【正例】**
1529
1530```cpp
1531void Foo1()
1532{
1533    std::string name{"demo"};
1534
1535    name = "test";
1536    name[1] = '2';
1537    ...
1538    Bar(name.c_str());
1539}
1540
1541void Foo2()
1542{
1543    std::string name{"demo"};
1544    std::string test{"test"};
1545    name += test;
1546    ...
1547    Bar(name.c_str());
1548}
1549
1550void Foo3(std::string& s)
1551{
1552    ...
1553    s.replace(0, 3, "***");
1554    ...
1555    Bar(s.data());
1556}
1557```
1558
1559**【例外】**
1560在少数对性能要求非常高的代码中,为了适配已有的只接受`const char*`类型入参的函数,可以临时保存std::string对象的c_str()方法返回的指针。但是必须严格保证std::string对象的生命周期长于所保存指针的生命周期,并且保证在所保存指针的生命周期内,std::string对象不会被修改。
1561
1562## 确保用于字符串操作的缓冲区有足够的空间容纳字符数据和结束符,并且字符串以null结束符结束
1563
1564**【描述】**
1565C风格字符串是一个连续的字符序列,由字符序列中的第一个出现的null字符终止并包含该null字符。
1566
1567复制或存储C风格字符串时,必须确保缓冲区有足够的空间容纳字符序列包括null结束符,并且字符串以null结束符结束,否则可能会导致缓冲区溢出问题:
1568
1569- 优先使用std::string表示字符串,std::string表示字符串操作更简便,更容易被正确的使用,避免由于C风格字符串使用不当而导致溢出、没有null结束符的问题。
1570- 使用C/C++标准库提供的C风格字符串操作函数时,需要确保输入的字符串以null结束符结束、不能超出字符串缓冲区的范围读写字符串、确保进存储操作后的字符串以null结束符结束。
1571
1572**【反例】**
1573
1574```cpp
1575char buf[BUFFER_SIZE];
1576std::cin >> buf;
1577void Foo(std::istream& in)
1578{
1579    char buffer[BUFFER_SIZE];
1580    if (!in.read(buffer, sizeof(buffer))) { // 注意:in.read()不能保证'\0'结尾
1581        ... // 错误处理
1582        return;
1583    }
1584
1585    std::string str(buffer);                // 不符合:字符串没有结尾的'\0'
1586    ...
1587}
1588void Foo(std::istream& in)
1589{
1590    std::string s;
1591    in >> s;                    // 不符合:没有限制待读取的长度,可能导致资源消耗或攻击
1592    ...
1593}
1594```
1595
1596**【正例】**
1597
1598```cpp
1599char buf[BUFFER_SIZE] = {0};
1600std::cin.width(sizeof(buf) - 1); // 注意需要缓冲区长度-1,以留出字符串末尾'\0'的空间
1601std::cin >> buf;
1602void Foo(std::istream& in)
1603{
1604    char buffer[BUFFER_SIZE];
1605
1606    if (!in.read(buffer, sizeof(buffer)) { // 注意in.read()不能保证'\0'结尾
1607        ... // 错误处理
1608        return;
1609    }
1610
1611    std::string str(buffer, in.gcount()); // 让std::string构造函数,只读取指定长度的字符内容
1612    ...
1613}
1614void Foo(std::istream& in)
1615{
1616    std::string s;
1617    in.width(MAX_NEED_SIZE);
1618    in >> s;                             // 符合:已经限制读取的最大长度
1619    ...
1620}
1621```
1622
1623**【影响】**
1624未对外部数据中的整数值进行限制可能导致拒绝服务,缓冲区溢出,信息泄露,或执行任意代码。
1625
1626## 禁止使用std::string存储敏感信息
1627
1628**【描述】**
1629std::string类是C++内部定义的字符串管理类,如果口令等敏感信息通过std::string进行操作,在程序运行过程中,敏感信息可能会散落到内存的各个地方,并且无法清除。
1630
1631**【反例】**
1632如下代码中,Foo函数中获取密码,保存到std::string变量password中,随后传递给VerifyPassword函数,在这个过程中,password实际上在内存中出现了两份。
1633
1634```cpp
1635bool VerifyPassword(std::string password)
1636{
1637    ...
1638}
1639
1640void Foo()
1641{
1642    std::string password = GetPassword();
1643    VerifyPassword(password);
1644}
1645```
1646
1647**【影响】**
1648未及时清理敏感信息,可能导致信息泄露。
1649
1650## 外部数据用于容器索引或迭代器时必须确保在有效范围内
1651
1652**【描述】**
1653外部数据是不可信数据,当将外部数据用于容器或数组的索引时,应确保其值在容器或数组可被访问元素的有效范围内;当将外部数据用于迭代器偏移时,应确保偏移后的迭代器值在与迭代器关联容器(从容器对象c的begin()方法创建)的[begin(), end())之间(即大于等于c.begin(),小于等于c.end())。
1654
1655对于具有at()方法的容器(如std::vector, std::set, std::map),对应索引越界或键值内容不存在时,方法将抛出异常;而其对应的operator[]出现索引越界时,将导致未定义行为;或者因键值内容不存在而构造对应键值的默认值不成功时,也将导致未定义行为。
1656
1657**【反例】**
1658
1659```cpp
1660int main()
1661{
1662    // 得到一个来自外部输入的整数 (index)
1663    int index;
1664    if (!(std::cin >> index)) {
1665        ... // 错误处理
1666        return -1;
1667    }
1668
1669    std::vector<char> c{'A', 'B', 'C', 'D'};
1670
1671    // 不符合:没有正确校验index的范围,溢出读取:需要确保index在容器元素的位置范围
1672    std::cout << c[index] << std::endl;
1673
1674    // 不符合:需要确保index在容器/数组元素的位置范围
1675    for (auto pos = std::cbegin(c) + index; pos < std::cend(c); ++pos) {
1676        std::cout << *pos << std::endl;
1677    }
1678    return 0;
1679}
1680void Foo(size_t n)
1681{
1682    std::vector<int> v{0, 1, 2, 3};
1683
1684    // n为外部的API传入的索引,可能导致越界访问
1685    for_each_n(v.cbegin(), n, [](int x) { std::cout << x; });
1686}
1687```
1688
1689**【正例】**
1690
1691```cpp
1692int main()
1693{
1694    // 得到一个来自外部输入的整数 (index)
1695    int index;
1696    if (!(std::cin >> index)) {
1697        ... // 错误处理
1698        return -1;
1699    }
1700
1701    // 这里仅以std::vector来举例,std::cbegin(c)等代码也适用于std::string字符串、
1702    // 和C数组(但不适应于char*变量以及char*表示的静态字符串)
1703    std::vector<char> c{'A', 'B', 'C', 'D'};
1704
1705    try {
1706        std::cout << c.at(index) << std::endl; // 符合:索引越界时,at函数将抛出异常
1707    } catch (const std::out_of_range& e) {
1708        ... // 越界异常处理
1709    }
1710
1711    // 后续代码必须使用检验合法的 index 进行容器元素索引或迭代器偏移
1712    // 正确校验index的范围:已确保index在容器元素的位置范围
1713    if (index < 0 || index >= c.size()) {
1714        ... // 错误处理
1715        return -1;
1716    }
1717
1718    std::cout << c[index] << std::endl;        // 符合:已检验index的范围
1719
1720    // 符合:已检验index
1721    for (auto pos = std::cbegin(c) + index; pos < std::cend(c); ++pos) {
1722        std::cout << *pos << std::endl;
1723    }
1724    return 0;
1725}
1726void Foo(size_t n)
1727{
1728    std::vector<int> v{0, 1, 2, 3};
1729
1730    // 必须确保for_each_n的迭代范围[first, first + count)有效
1731    if (n > v.size()) {
1732        ... // 错误处理
1733        return;
1734    }
1735    for_each_n(v.cbegin(), n, [](int x) { std::cout << x; });
1736}
1737```
1738
1739## 调用格式化输入/输出函数时,使用有效的格式字符串
1740
1741**【描述】**
1742使用C风格的格式化输入/输出函数时,需要确保格式串是合法有效的,并且格式串与相应的实参类型是严格匹配的,否则会使程序产生非预期行为。
1743
1744除C风格的格式化输入/输出函数以外,C++中类似的函数也需要确保使用有效的格式串,如C++20的std::format()函数。
1745
1746对于自定义C风格的格式化函数,可以使用编译器支持的属性自动检查使用自定义格式化函数的正确性。
1747例如:GCC支持自动检测类似printf, scanf, strftime, strfmon的自定义格式化函数,参考GCC手册的Common Function Attributes:
1748
1749```c
1750extern int CustomPrintf(void* obj, const char* format, ...)
1751    __attribute__ ((format (printf, 2, 3)));
1752```
1753
1754**【反例】**
1755如下代码示例中,格式化输入一个整数到macAddr变量中,但是macAddr为unsigned char类型,而%x对应的是int类型参数,函数执行完成后会发生写越界。
1756
1757```c
1758unsigned char macAddr[6];
1759...
1760// macStr中的数据格式为 e2:42:a4:52:1e:33
1761int ret = sscanf(macStr, "%x:%x:%x:%x:%x:%x\n",
1762                  &macAddr[0], &macAddr[1],
1763                  &macAddr[2], &macAddr[3],
1764                  &macAddr[4], &macAddr[5]);
1765...
1766```
1767
1768**【正例】**
1769如下代码中,使用%hhx确保格式串与相应的实参类型严格匹配。
1770
1771```c
1772unsigned char macAddr[6];
1773...
1774// macStr中的数据格式为 e2:42:a4:52:1e:33
1775int ret = sscanf(macStr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx\n",
1776                  &macAddr[0], &macAddr[1],
1777                  &macAddr[2], &macAddr[3],
1778                  &macAddr[4], &macAddr[5]);
1779...
1780```
1781
1782注:在C++中不推荐使用sscanf, sprintf等C库函数,可以替换为:std::istringstream, std::ostringstream, std::stringstream等。
1783
1784**【影响】**
1785错误的格式串可能造成内存破坏或者程序异常终止。
1786
1787## 调用格式化输入/输出函数时,禁止format参数受外部数据控制
1788
1789**【描述】**
1790调用格式化函数时,如果format参数由外部数据提供,或由外部数据拼接而来,会造成字符串格式化漏洞。
1791以C标准库的格式化输出函数为例,当其format参数外部可控时,攻击者可以使用%n转换符向指定地址写入一个整数值、使用%x或%d转换符查看栈或寄存器内容、使用%s转换符造成进程崩溃等。
1792
1793常见格式化函数有:
1794
1795- 格式化输出函数: sprintf, vsprintf, snprintf, vsnprintf等等
1796- 格式化输入函数: sscanf, vsscanf, fscanf, vscanf等等
1797- 格式化错误消息函数: err(), verr(), errx(), verrx(), warn(), vwarn(), warnx(), vwarnx(), error(), error_at_line()
1798- 格式化日志函数: syslog(), vsyslog()
1799- C++20提供的std::format()
1800
1801调用格式化函数时,应使用常量字符串作为格式串,禁止格式串外部可控:
1802
1803```cpp
1804Box<int> v{MAX_COUNT};
1805std::cout << std::format("{:#x}", v);
1806```
1807
1808**【反例】**
1809如下代码示例中,使用Log()函数直接打印外部数据,可能出现格式化字符串漏洞。
1810
1811```c
1812void Foo()
1813{
1814    std::string msg = GetMsg();
1815    ...
1816    syslog(priority, msg.c_str());       // 不符合:存在格式化字符串漏洞
1817}
1818```
1819
1820**【正例】**
1821下面是推荐做法,使用%s转换符打印外部数据,避免格式化字符串漏洞。
1822
1823```c
1824void Foo()
1825{
1826    std::string msg = GetMsg();
1827    ...
1828    syslog(priority, "%s", msg.c_str()); // 符合:这里没有格式化字符串漏洞
1829}
1830```
1831
1832**【影响】**
1833如果格式串被外部可控,攻击者可以使进程崩溃、查看栈内容、查看内存内容或者在任意内存位置写入数据,进而以被攻击进程的权限执行任意代码。
1834
1835## 禁止外部可控数据作为进程启动函数的参数或者作为dlopen/LoadLibrary等模块加载函数的参数
1836
1837**【描述】**
1838本条款中进程启动函数包括system、popen、execl、execlp、execle、execv、execvp等。
1839system()、popen()等函数会创建一个新的进程,如果外部可控数据作为这些函数的参数,会导致注入漏洞。
1840使用execl()等函数执行新进程时,如果使用shell启动新进程,则同样存在命令注入风险。
1841使用execlp()、execvp()、execvpe()函数依赖于系统的环境变量PATH来搜索程序路径,使用它们时应充分考虑外部环境变量的风险,或避免使用这些函数。
1842
1843因此,总是优先考虑使用C标准函数实现需要的功能。如果确实需要使用这些函数,应使用白名单机制确保这些函数的参数不受任何外来数据的影响。
1844
1845dlopen、LoadLibrary函数会加载外部模块,如果外部可控数据作为这些函数的参数,有可能会加载攻击者事先预制的模块。如果要使用这些函数,可以采用如下措施之一:
1846
1847- 使用白名单机制,确保这些函数的参数不受任何外来数据的影响。
1848- 使用数字签名机制保护要加载的模块,充分保证其完整性。
1849- 在设备本地加载的动态库通过权限与访问控制措施保证了本身安全性后,通过特定目录自动被程序加载。
1850- 在设备本地的配置文件通过权限与访问控制措施保证了本身安全性后,自动加载配置文件中指定的动态库。
1851
1852**【反例】**
1853如下代码从外部获取数据后直接作为LoadLibrary函数的参数,有可能导致程序被植入木马。
1854
1855```c
1856char* msg = GetMsgFromRemote();
1857LoadLibrary(msg);
1858```
1859
1860如下代码示例中,使用 system() 函数执行 cmd 命令串来自外部,攻击者可以执行任意命令:
1861
1862```c
1863std::string cmd = GetCmdFromRemote();
1864system(cmd.c_str());
1865```
1866
1867如下代码示例中,使用 system() 函数执行 cmd 命令串的一部分来自外部,攻击者可能输入 `some dir;reboot`字符串,创造成系统重启:
1868
1869```cpp
1870std::string name = GetDirNameFromRemote();
1871std::string cmd{"ls " + name};
1872system(cmd.c_str());
1873```
1874
1875使用exec系列函数来避免命令注入时,注意exec系列函数中的path、file参数禁止使用命令解析器(如/bin/sh)。
1876
1877```c
1878int execl(const char* path, const char* arg, ...);
1879int execlp(const char* file, const char* arg, ...);
1880int execle(const char* path, const char* arg, ...);
1881int execv(const char* path, char* const argv[]);
1882int execvp(const char* file, char* const argv[]);
1883int execvpe(const char* file, char* const argv[], char* const envp[]);
1884```
1885
1886例如,禁止如下使用方式:
1887
1888```c
1889std::string cmd = GetDirNameFromRemote();
1890execl("/bin/sh", "sh", "-c", cmd.c_str(), nullptr);
1891```
1892
1893可以使用库函数,或者可以通过编写少量的代码来避免使用system函数调用命令,如`mkdir()`函数可以实现`mkdir`命令的功能。
1894如下代码中,应该避免使用`cat`命令实现文件内容复制的功能。
1895
1896```c
1897int WriteDataToFile(const char* dstFile, const char* srcFile)
1898{
1899    ...  // 入参的合法性校验
1900    std::ostringstream oss;
1901    oss << "cat " << srcFile << " > " << dstFile;
1902
1903    std::string cmd{oss.str()};
1904    system(cmd.c_str());
1905    ...
1906}
1907```
1908
1909**【正例】**
1910
1911如下代码中,通过少量的代码来实现。如下代码实现了文件复制的功能,避免了对`cat`或`cp`命令的调用。需要注意的是,为简化描述,下面代码未考虑信号中断的影响。
1912
1913```cpp
1914bool WriteDataToFile(const std::string& dstFilePath, const std::string& srcFilePath)
1915{
1916    const int bufferSize = 1024;
1917    std::vector<char> buffer (bufferSize + 1, 0);
1918
1919    std::ifstream srcFile(srcFilePath, std::ios::binary);
1920    std::ofstream dstFile(dstFilePath, std::ios::binary);
1921
1922    if (!dstFile || !dstFile) {
1923        ... // 错误处理
1924        return false;
1925    }
1926
1927    while (true) {
1928        // 从srcFile读取内容分块
1929        srcFile.read(buffer.data(), bufferSize);
1930        std::streamsize size = srcFile ? bufferSize : srcFile.gcount();
1931
1932        // 写入分块内容到dstFile
1933        if (size > 0 && !dstFile.write(buffer.data(), size)) {
1934            ... // 错误处理
1935            break;
1936        }
1937
1938        if (!srcFile) {
1939            ... // 检查错误:当不是eof()时记录错误
1940            break;
1941        }
1942    }
1943    // srcFile 和 dstFile 在退出作用域时会自动被关闭
1944    return true;
1945}
1946```
1947
1948可以通过库函数简单实现的功能(如上例),需要避免调用命令处理器来执行外部命令。
1949如果确实需要调用单个命令,应使用exec*函数来实现参数化调用,并对调用的命令实施白名单管理。同时应避免使用execlp、execvp、execvpe函数,因为这几个函数依赖外部的PATH环境变量。
1950此时,外部输入的fileName仅作为some_tool命令的参数,没有命令注入的风险。
1951
1952```cpp
1953pid_t pid;
1954char* const envp[] = {nullptr};
1955...
1956std::string fileName = GetDirNameFromRemote();
1957...
1958pid = fork();
1959if (pid < 0) {
1960    ...
1961} else if (pid == 0) {
1962    // 使用some_tool对指定文件进行加工
1963    execle("/bin/some_tool", "some_tool", fileName.c_str(), nullptr, envp);
1964    _Exit(-1);
1965}
1966...
1967int status;
1968waitpid(pid, &status, 0);
1969std::ofstream ofs(fileName, std::ios::in);
1970...
1971```
1972
1973在必须使用system等命令解析器执行命令时,应对输入的命令字符串基于合理的白名单检查,避免命令注入。
1974
1975```cpp
1976std::string cmd = GetCmdFromRemote();
1977
1978// 使用白名单检查命令是否合法,仅允许"some_tool_a", "some_tool_b"命令,外部无法随意控制
1979if (!IsValidCmd(cmd.c_str())) {
1980    ... // 错误处理
1981}
1982system(cmd.c_str());
1983...
1984```
1985
1986**【影响】**
1987
1988- 如果传递给system()、popen()或其他命令处理函数的命令字符串是外部可控的,则攻击者可能会以被攻击进程的权限执行系统上存在的任意命令。
1989- 如果动态库文件是外部可控的,则攻击者可替换该库文件,在某些情况下可以造成任意代码执行漏洞。
1990
1991# 其他C语言编程规范
1992
1993## 禁止通过对数组类型的函数参数变量进行sizeof来获取数组大小
1994
1995**【描述】**
1996
1997使用sizeof操作符求其操作数的大小(以字节为单位),其操作数可以是一个表达式或者加上括号的类型名称,例如:`sizeof(int)`或`sizeof(int *)`。
1998参考C11标准6.5.3.4中的脚注103:
1999
2000> 当将sizeof应用于具有数组或函数类型的参数时,sizeof操作符将得出调整后的(指针)类型的大小。
2001
2002函数参数列表中声明为数组的参数会被调整为相应类型的指针。例如:`void Func(int inArray[LEN])`函数参数列表中的inArray虽然被声明为数组,但是实际上会被调整为指向int类型的指针,即调整为`void Func(int *inArray)`。
2003在这个函数内使用`sizeof(inArray)`等同于`sizeof(int *)`,得到的结果通常与预期不相符。例如:在IA-32架构上,`sizeof(inArray)` 的值是 4,并不是inArray数组的大小。
2004
2005**【反例】**
2006如下代码示例中,函数ArrayInit的功能是初始化数组元素。该函数有一个声明为`int inArray[]`的参数,被调用时传递了一个长度为256的int类型数组data。
2007ArrayInit函数实现中使用`sizeof(inArray) / sizeof(inArray[0])`方法来计算入参数组中元素的数量。
2008但由于inArray是函数参数,所以具有指针类型,结果,`sizeof(inArray)`等同于`sizeof(int *)`。
2009无论传递给ArrayInit函数的数组实际长度如何,表达式的`sizeof(inArray) / sizeof(inArray[0])`计算结果均为1,与预期不符。
2010
2011```c
2012#define DATA_LEN 256
2013void ArrayInit(int inArray[])
2014{
2015    // 不符合:这里使用sizeof(inArray)计算数组大小
2016    for (size_t i = 0; i < sizeof(inArray) / sizeof(inArray[0]); i++) {
2017        ...
2018    }
2019}
2020
2021void FunctionData(void)
2022{
2023    int data[DATA_LEN];
2024
2025    ...
2026    ArrayInit(data); // 调用ArrayInit函数初始化数组data数据
2027    ...
2028}
2029```
2030
2031**【正例】**
2032如下代码示例中,修改函数定义,添加数组长度参数,并在调用处正确传入数组长度。
2033
2034```c
2035#define DATA_LEN 256
2036// 函数说明:入参len是入参inArray数组的长度
2037void ArrayInit(int inArray[], size_t len)
2038{
2039    for (size_t i = 0; i < len; i++) {
2040        ...
2041    }
2042}
2043
2044void FunctionData(void)
2045{
2046    int data[DATA_LEN];
2047
2048    ArrayInit(data, sizeof(data) / sizeof(data[0]));
2049    ...
2050}
2051```
2052
2053**【反例】**
2054如下代码示例中,`sizeof(inArray)`不等于`ARRAY_MAX_LEN * sizeof(int)`,因为将sizeof操作符应用于声明为具有数组类型的参数时,即使参数声明指定了长度,也会被调整为指针,`sizeof(inArray)`等同于 `sizeof(int *)`:
2055
2056```c
2057#define ARRAY_MAX_LEN 256
2058
2059void ArrayInit(int inArray[ARRAY_MAX_LEN])
2060{
2061    // 不符合:sizeof(inArray),得到的长度是指针的大小,不是数组的长度,和预期不符。
2062    for (size_t i = 0; i < sizeof(inArray) / sizeof(inArray[0]); i++) {
2063        ...
2064    }
2065}
2066
2067int main(void)
2068{
2069    int masterArray[ARRAY_MAX_LEN];
2070
2071    ...
2072    ArrayInit(masterArray);
2073
2074    return 0;
2075}
2076```
2077
2078**【正例】**
2079如下代码示例中,使用入参len表示指定数组的长度:
2080
2081```c
2082#define ARRAY_MAX_LEN 256
2083
2084// 函数说明:入参len是入参数组的长度
2085void ArrayInit(int inArray[], size_t len)
2086{
2087    for (size_t i = 0; i < len; i++) {
2088        ...
2089    }
2090}
2091
2092int main(void)
2093{
2094    int masterArray[ARRAY_MAX_LEN];
2095
2096    ArrayInit(masterArray, ARRAY_MAX_LEN);
2097    ...
2098
2099    return 0;
2100}
2101```
2102
2103## 禁止通过对指针变量进行sizeof操作来获取数组大小
2104
2105**描述】**
2106将指针当做数组进行sizeof操作时,会导致实际的执行结果与预期不符。例如:变量定义 `char *p = array`,其中array的定义为`char array[LEN]`,表达式`sizeof(p)`得到的结果与 `sizeof(char *)`相同,并非array的长度。
2107
2108**【反例】**
2109如下代码示例中,buffer和path分别是指针和数组,程序员想对这2个内存进行清0操作,但由于程序员的疏忽,将内存大小误写成了`sizeof(buffer)`,与预期不符。
2110
2111```c
2112char path[MAX_PATH];
2113char *buffer = (char *)malloc(SIZE);
2114...
2115
2116...
2117memset(path, 0, sizeof(path));
2118
2119// sizeof与预期不符,其结果为指针本身的大小而不是缓冲区大小
2120memset(buffer, 0, sizeof(buffer));
2121```
2122
2123**【正例】**
2124如下代码示例中,将`sizeof(buffer)`修改为申请的缓冲区大小:
2125
2126```c
2127char path[MAX_PATH];
2128char *buffer = (char *)malloc(SIZE);
2129...
2130
2131...
2132memset(path, 0, sizeof(path));
2133memset(buffer, 0, SIZE); // 使用申请的缓冲区大小
2134```
2135
2136## 禁止直接使用外部数据拼接SQL命令
2137
2138**【描述】**
2139SQL注入是指SQL查询被恶意更改成一个与程序预期完全不同的查询。执行更改后的查询可能会导致信息泄露或者数据被篡改。而SQL注入的根源就是使用外部数据来拼接SQL语句。C/C++语言中常见的使用外部数据拼接SQL语句的场景有(包括但不局限于):
2140
2141- 连接MySQL时调用mysql_query(),Execute()时的入参
2142- 连接SQL Server时调用db-library驱动的dbsqlexec()的入参
2143- 调用ODBC驱动的SQLprepare()连接数据库时的SQL语句的参数
2144- C++程序调用OTL类库中的otl_stream(),otl_column_desc()时的入参
2145- C++程序连接Oracle数据库时调用ExecuteWithResSQL()的入参
2146
2147防止SQL注入的方法主要有以下几种:
2148
2149- 参数化查询(通常也叫作预处理语句):参数化查询是一种简单有效的防止SQL注入的查询方式,应该被优先考虑使用。支持的数据库有MySQL,Oracle(OCI)。
2150- 参数化查询(通过ODBC驱动):支持ODBC驱动参数化查询的数据库有Oracle、SQLServer、PostgreSQL和GaussDB。
2151- 对外部数据进行校验(对于每个引入的外部数据推荐“白名单”校验)。
2152- 对外部数据中的SQL特殊字符进行转义。
2153
2154**【反例】**
2155下列代码拼接用户输入,没有进行输入检查,存在SQL注入风险:
2156
2157```c
2158char name[NAME_MAX];
2159char sqlStatements[SQL_CMD_MAX];
2160int ret = GetUserInput(name, NAME_MAX);
2161...
2162ret = sprintf(sqlStatements,
2163                "SELECT childinfo FROM children WHERE name= ‘%s’",
2164                name);
2165...
2166ret = mysql_query(&myConnection, sqlStatements);
2167...
2168```
2169
2170**【正例】**
2171使用预处理语句进行参数化查询可以防御SQL注入攻击:
2172
2173```c
2174char name[NAME_MAX];
2175...
2176MYSQL_STMT *stmt = mysql_stmt_init(myConnection);
2177char *query = "SELECT childinfo FROM children WHERE name= ?";
2178if (mysql_stmt_prepare(stmt, query, strlen(query))) {
2179    ...
2180}
2181int ret = GetUserInput(name, NAME_MAX);
2182...
2183MYSQL_BIND params[1];
2184(void)memset(params, 0, sizeof(params));
2185...
2186params[0].bufferType = MYSQL_TYPE_STRING;
2187params[0].buffer = (char *)name;
2188params[0].bufferLength = strlen(name);
2189params[0].isNull = 0;
2190
2191bool isCompleted = mysql_stmt_bind_param(stmt, params);
2192...
2193ret = mysql_stmt_execute(stmt);
2194...
2195```
2196
2197**【影响】**
2198
2199如果拼接SQL语句的字符串是外部可控的,则攻击者可以通过注入特定的字符串欺骗程序执行恶意的SQL命令,造成信息泄露、权限绕过、数据被篡改等问题。
2200
2201## 内存中的敏感信息使用完毕后立即清0
2202
2203**【描述】**
2204内存中的口令、密钥等敏感信息使用完毕后立即清0,避免被攻击者获取或者无意间泄露给低权限用户。这里所说的内存包括但不限于:
2205
2206- 动态分配的内存
2207- 静态分配的内存
2208- 自动分配(堆栈)内存
2209- 内存缓存
2210- 磁盘缓存
2211
2212**【反例】**
2213通常内存在释放前不需要清除内存数据,因为这样在运行时会增加额外开销,所以在这段内存被释放之后,之前的数据还是会保留在其中。如果这段内存中的数据包含敏感信息,则可能会意外泄露敏感信息。为了防止敏感信息泄露,必须先清除内存中的敏感信息,然后再释放。
2214在如下代码示例中,存储在所引用的动态内存中的敏感信息secret被复制到新动态分配的缓冲区newSecret,最终通过free()释放。因为释放前未清除这块内存数据,这块内存可能被重新分配到程序的另一部分,之前存储在newSecret中的敏感信息可能会无意中被泄露。
2215
2216```c
2217char *secret = NULL;
2218/*
2219 * 假设 secret 指向敏感信息,敏感信息的内容是长度小于SIZE_MAX个字符,
2220 * 并且以null终止的字节字符串
2221 */
2222
2223size_t size = strlen(secret);
2224char *newSecret = NULL;
2225newSecret = (char *)malloc(size + 1);
2226if (newSecret == NULL) {
2227    ... // 错误处理
2228} else {
2229    errno_t ret = strcpy(newSecret, secret);
2230    ... // 处理 ret
2231
2232    ... // 处理 newSecret...
2233
2234    free(newSecret);
2235    newSecret = NULL;
2236}
2237...
2238```
2239
2240**【正例】**
2241如下代码示例中,为了防止信息泄露,应先清除包含敏感信息的动态内存(用’\0’字符填充空间),然后再释放它。
2242
2243```c
2244char *secret = NULL;
2245/*
2246 * 假设 secret 指向敏感信息,敏感信息的内容是长度小于SIZE_MAX个字符,
2247 * 并且以null终止的字节字符串
2248 */
2249size_t size = strlen(secret);
2250char *newSecret = NULL;
2251newSecret = (char *)malloc(size + 1);
2252if (newSecret == NULL) {
2253    ... // 错误处理
2254} else {
2255    errno_t ret = strcpy(newSecret,  secret);
2256    ... // 处理 ret
2257
2258    ... // 处理 newSecret...
2259
2260    (void)memset(newSecret,  0, size + 1);
2261    free(newSecret);
2262    newSecret = NULL;
2263}
2264...
2265```
2266
2267**【正例】**
2268下面是另外一个涉及敏感信息清理的场景,在代码获取到密码后,将密码保存到password中,进行密码验证,使用完毕后,通过`memset()`函数对password清0。
2269
2270```c
2271int Foo(void)
2272{
2273    char password[MAX_PWD_LEN];
2274    if (!GetPassword(password, sizeof(password))) {
2275        ...
2276    }
2277    if (!VerifyPassword(password)) {
2278        ...
2279    }
2280    ...
2281    (void)memset(password,  0, sizeof(password));
2282    ...
2283}
2284```
2285
2286要特别**注意**:对敏感信息清理的时候要同时防止因编译器优化而使清理代码无效。
2287
2288例如,下列代码使用了可能被编译器优化掉的语句。
2289
2290```c
2291int SecureLogin(void)
2292{
2293    char pwd[PWD_SIZE];
2294    if (RetrievePassword(pwd, sizeof(pwd))) {
2295        ... // 口令检查及其他处理
2296    }
2297    memset(pwd, 0, sizeof(pwd)); // 编译器优化有可能会使该语句失效
2298    ...
2299}
2300```
2301
2302某些编译器在优化时候不会执行它认为不会改变程序执行结果的代码,因此memset()操作会被优化掉。
2303
2304如果编译器支持#pragma指令,那么可以使用该指令指示编译器不作优化。
2305
2306```c
2307void SecureLogin(void)
2308{
2309    char pwd[PWD_SIZE];
2310    if (RetrievePassword(pwd, sizeof(pwd))) {
2311        ... // 口令检查及其他处理
2312    }
2313    #pragma optimize("", off)
2314    // 清除内存
2315    ...
2316    #pragma optimize("", on)
2317    ...
2318}
2319```
2320
2321**【影响】**
2322
2323未及时清理敏感信息,可能导致信息泄露。
2324
2325## 创建文件时必须显式指定合适的文件访问权限
2326
2327**【描述】**
2328创建文件时,如果不显式指定合适访问权限,可能会让未经授权的用户访问该文件,造成信息泄露,文件数据被篡改,文件中被注入恶意代码等风险。
2329
2330虽然文件的访问权限也依赖于文件系统,但是当前许多文件创建函数(例如POSIX open函数)都具有设置(或影响)文件访问权限的功能,所以当使用这些函数创建文件时,必须显式指定合适的文件访问权限,以防止意外访问。
2331
2332**【反例】**
2333使用POSIX open()函数创建文件但未显示指定该文件的访问权限,可能会导致文件创建时具有过高的访问权限。这可能会导致漏洞(例如CVE-2006-1174)。
2334
2335```c
2336void Foo(void)
2337{
2338    int fd = -1;
2339    char *filename = NULL;
2340
2341    ... // 初始化 filename
2342
2343    fd = open(filename, O_CREAT | O_WRONLY); // 没有显式指定访问权限
2344    if (fd == -1) {
2345        ... // 错误处理
2346    }
2347    ...
2348}
2349```
2350
2351**【正例】**
2352应该在open的第三个参数中显式指定新创建文件的访问权限。可以根据文件实际的应用情况设置何种访问权限。
2353
2354```c
2355void Foo(void)
2356{
2357    int fd = -1;
2358    char *filename = NULL;
2359
2360    ... // 初始化 filename 和指定其访问权限
2361
2362    // 此处根据文件实际需要,显式指定其访问权限
2363    int fd = open(filename, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR);
2364    if (fd == -1) {
2365        ... // 错误处理
2366    }
2367    ...
2368}
2369```
2370
2371**【影响】**
2372
2373创建访问权限弱的文件,可能会导致对这些文件的非法访问。
2374
2375## 使用文件路径前必须进行规范化并校验
2376
2377**【描述】**
2378当文件路径来自外部数据时,必须对其做合法性校验,如果不校验,可能造成系统文件的被任意访问。但是禁止直接对其进行校验,正确做法是在校验之前必须对其进行路径规范化处理。这是因为同一个文件可以通过多种形式的路径来描述和引用,例如既可以是绝对路径,也可以是相对路径;而且路径名、目录名和文件名可能包含使校验变得困难和不准确的字符(如:“.”、“..”)。此外,文件还可以是符号链接,这进一步模糊了文件的实际位置或标识,增加了校验的难度和校验准确性。所以必须先将文件路径规范化,从而更容易校验其路径、目录或文件名,增加校验准确性。
2379
2380因为规范化机制在不同的操作系统和文件系统之间可能有所不同,所以最好使用符合当前系统特性的规范化机制。
2381
2382一个简单的案例说明如下:
2383
2384```c
2385当文件路径来自外部数据时,需要先将文件路径规范化,如果没有作规范化处理,攻击者就有机会通过恶意构造文件路径进行文件的越权访问。
2386例如,攻击者可以构造“../../../etc/passwd”的方式进行任意文件访问。
2387```
2388
2389**【反例】**
2390在此错误的示例中,inputFilename包含一个源于受污染源的文件名,并且该文件名已打开以进行写入。在使用此文件名操作之前,应该对其进行验证,以确保它引用的是预期的有效文件。
2391不幸的是,inputFilename引用的文件名可能包含特殊字符,例如目录字符,这使验证变得困难,甚至不可能。而且,inputFilename中可能包含可以指向任意文件路径的符号链接,即使该文件名通过了验证,也会导致该文件名是无效的。
2392这种场景下,对文件名的直接验证即使被执行也是得不到预期的结果,对fopen()的调用可能会导致访问一个意外的文件。
2393
2394```c
2395...
2396
2397if (!verify_file(inputFilename) {    // 没有对inputFilename做规范化,直接做校验
2398    ... // 错误处理
2399}
2400
2401if (fopen(inputFilename, "w") == NULL) {
2402    ... // 错误处理
2403}
2404
2405...
2406```
2407
2408**【正例】**
2409规范化文件名是具有一定难度的,因为这需要了解底层文件系统。
2410POSIX realpath()函数可以帮助将路径名转换为规范形式。参考信息技术标准-POSIX®,基本规范第7期[IEEE std 1003.1:2013]:
2411
2412- 该realpath()函数应从所指向的路径名派生一个filename的绝对路径名,两者指向同一文件,绝对路径其文件名不涉及“ .”,“ ..”或符号链接。
2413  在规范化路径之后,还必须执行进一步的验证,例如确保两个连续的斜杠或特殊文件不会出现在文件名中。有关如何执行路径名解析的更多详细信息,请参见[IEEE Std 1003.1: 2013]第4.12节“路径名解析”。
2414  使用realpath()函数有许多需要注意的地方。
2415  在了解了以上原理之后,对上面的错误代码示例,我们采用如下解决方案:
2416
2417```c
2418char *realpathRes = NULL;
2419
2420...
2421
2422// 在校验之前,先对inputFilename做规范化处理
2423realpathRes = realpath(inputFilename, NULL);
2424if (realpathRes == NULL) {
2425    ... // 规范化的错误处理
2426}
2427
2428// 规范化以后对路径进行校验
2429if (!verify_file(realpathRes) {
2430    ... // 校验的错误处理
2431}
2432
2433// 使用
2434if (fopen(realpathRes, "w") == NULL) {
2435    ... // 实际操作的错误处理
2436}
2437
2438...
2439
2440free(realpathRes);
2441realpathRes = NULL;
2442...
2443```
2444
2445**【正例】**
2446根据我们的实际场景,我们还可以采用的第二套解决方案,说明如下:
2447如果`PATH_MAX`被定义为 limits.h 中的一个常量,那么使用非空的`resolved_path`调用realpath()也是安全的。
2448在本例中realpath()函数期望`resolved_path`引用一个字符数组,该字符数组足够大,可以容纳规范化的路径。
2449如果定义了PATH_MAX,则分配一个大小为`PATH_MAX`的缓冲区来保存realpath()的结果。正确代码示例如下:
2450
2451```c
2452char *realpathRes = NULL;
2453char *canonicalFilename = NULL;
2454size_t pathSize = 0;
2455
2456...
2457
2458pathSize = (size_t)PATH_MAX;
2459
2460if (VerifyPathSize(pathSize)) {
2461    canonicalFilename = (char *)malloc(pathSize);
2462
2463    if (canonicalFilename == NULL) {
2464        ... // 错误处理
2465    }
2466
2467    realpathRes = realpath(inputFilename, canonicalFilename);
2468}
2469
2470if (realpathRes == NULL) {
2471    ... // 错误处理
2472}
2473
2474if (VerifyFile(realpathRes)) {
2475    ... // 错误处理
2476}
2477
2478if (fopen(realpathRes, "w") == NULL ) {
2479    ... // 错误处理
2480}
2481
2482...
2483
2484free(canonicalFilename);
2485canonicalFilename = NULL;
2486...
2487```
2488
2489**【反例】**
2490下面的代码场景是从外部获取到文件名称,拼接成文件路径后,直接对文件内容进行读取,导致攻击者可以读取到任意文件的内容:
2491
2492```c
2493char *filename = GetMsgFromRemote();
2494...
2495int ret = sprintf(untrustPath,  "/tmp/%s", filename);
2496...
2497char *text = ReadFileContent(untrustPath);
2498```
2499
2500**【正例】**
2501正确的做法是,对路径进行规范化后,再判断路径是否是本程序所认为的合法的路径:
2502
2503```c
2504char *filename = GetMsgFromRemote();
2505...
2506sprintf(untrustPath,  "/tmp/%s", filename);
2507char path[PATH_MAX];
2508if (realpath(untrustPath, path) == NULL) {
2509    ... // 处理错误
2510}
2511if (!IsValidPath(path)) {    // 检查文件的位置是否正确
2512    ... // 处理错误
2513}
2514char *text = ReadFileContent(path);
2515```
2516
2517**【例外】**
2518
2519运行于控制台的命令行程序,通过控制台手工输入文件路径,可以作为本条款例外。
2520
2521```c
2522int main(int argc, char **argv)
2523{
2524    int fd = -1;
2525
2526    if (argc == 2) {
2527        fd = open(argv[1], O_RDONLY);
2528        ...
2529    }
2530
2531    ...
2532    return 0;
2533}
2534```
2535
2536**【影响】**
2537
2538未对不可信的文件路径进行规范化和校验,可能造成对任意文件的访问。
2539
2540## 不要在共享目录中创建临时文件
2541
2542**【描述】**
2543共享目录是指其它非特权用户可以访问的目录。程序的临时文件应当是程序自身独享的,任何将自身临时文件置于共享目录的做法,将导致其他共享用户获得该程序的额外信息,产生信息泄露。因此,不要在任何共享目录创建仅由程序自身使用的临时文件。
2544
2545临时文件通常用于辅助保存不能驻留在内存中的数据或存储临时的数据,也可用作进程间通信的一种手段(通过文件系统传输数据)。例如,一个进程在共享目录中创建一个临时文件,该文件名可能使用了众所周知的名称或者一个临时的名称,然后就可以通过该文件在进程间共享信息。这种通过在共享目录中创建临时文件的方法实现进程间共享的做法很危险,因为共享目录中的这些文件很容易被攻击者劫持或操纵。这里有几种缓解策略:
2546
25471. 使用其他低级IPC(进程间通信)机制,例如套接字或共享内存。
25482. 使用更高级别的IPC机制,例如远程过程调用。
25493. 使用仅能由程序本身访问的安全目录(多线程/进程下注意防止条件竞争)。
2550
2551同时,下面列出了几项临时文件创建使用的方法,产品根据具体场景执行以下一项或者几项,同时产品也可以自定义合适的方法。
2552
25531. 文件必须具有合适的权限,只有符合权限的用户才能访问
25542. 创建的文件名是唯一的、或不可预测的
25553. 仅当文件不存在时才创建打开(原子创建打开)
25564. 使用独占访问打开,避免竞争条件
25575. 在程序退出之前移除
2558
2559同时也需要注意到,当某个目录被开放读/写权限给多个用户或者一组用户时,该共享目录潜在的安全风险远远大于访问该目录中临时文件这个功能的本身。
2560
2561在共享目录中创建临时文件很容易受到威胁。例如,用于本地挂载的文件系统的代码在与远程挂载的文件系统一起共享使用时可能会受到攻击。安全的解决方案是不要在共享目录中创建临时文件。
2562
2563**【反例】**
2564如下代码示例,程序在系统的共享目录/tmp下创建临时文件来保存临时数据,且文件名是硬编码的。
2565由于文件名是硬编码的,因此是可预测的,攻击者只需用符号链接替换文件,然后链接所引用的目标文件就会被打开并写入新内容。
2566
2567```c
2568void ProcData(const char *filename)
2569{
2570    FILE *fp = fopen(filename, "wb+");
2571    if (fp == NULL) {
2572        ... // 错误处理
2573    }
2574
2575    ... // 写文件
2576
2577    fclose(fp);
2578}
2579
2580int main(void)
2581{
2582    // 不符合:1.在系统共享目录中创建临时文件;2.临时文件名硬编码
2583    char *pFile = "/tmp/data";
2584    ...
2585
2586    ProcData(pFile);
2587
2588    ...
2589    return 0;
2590}
2591```
2592
2593**【正确案例】**
2594
2595```c
2596不应在该目录下创建仅由程序自身使用的临时文件。
2597```
2598
2599**【影响】**
2600
2601不安全的创建临时文件,可能导致文件非法访问,并造成本地系统上的权限提升。
2602
2603## 不要在信号处理函数中访问共享对象
2604
2605**【描述】**
2606如果在信号处理程序中访问和修改共享对象,可能会造成竞争条件,使数据处于不确定的状态。
2607这条规则有两个不适用的场景(参考C11标准5.1.2.3第5段):
2608
2609- 读写不需要加锁的原子对象;
2610- 读写volatile sig_atomic_t类型的对象,因为具有volatile sig_atomic_t类型的对象即使在出现异步中断的时候也可以作为一个原子实体访问,是异步安全的。
2611
2612**【反例】**
2613在这个信号处理过程中,程序打算将`g_msg`作为共享对象,当产生SIGINT信号时更新共享对象的内容,但是该`g_msg`变量类型不是`volatile sig_atomic_t`,所以不是异步安全的。
2614
2615```c
2616#define MAX_MSG_SIZE 32
2617static char g_msgBuf[MAX_MSG_SIZE] = {0};
2618static char *g_msg = g_msgBuf;
2619
2620void SignalHandler(int signum)
2621{
2622    // 下面代码操作g_msg不合规,因为不是异步安全的
2623    (void)memset(g_msg,0, MAX_MSG_SIZE);
2624    errno_t ret = strcpy(g_msg,  "signal SIGINT received.");
2625    ... // 处理 ret
2626}
2627
2628int main(void)
2629{
2630    errno_t ret = strcpy(g_msg,  "No msg yet."); // 初始化消息内容
2631    ... // 处理 ret
2632
2633    signal(SIGINT, SignalHandler); // 设置SIGINT信号对应的处理函数
2634
2635    ... // 程序主循环代码
2636
2637    return 0;
2638}
2639```
2640
2641**【正例】**
2642如下代码示例中,在信号处理函数中仅将`volatile sig_atomic_t`类型作为共享对象使用。
2643
2644```c
2645#define MAX_MSG_SIZE 32
2646volatile sig_atomic_t g_sigFlag = 0;
2647
2648void SignalHandler(int signum)
2649{
2650    g_sigFlag = 1; // 符合
2651}
2652
2653int main(void)
2654{
2655    signal(SIGINT, SignalHandler);
2656    char msgBuf[MAX_MSG_SIZE];
2657    errno_t ret = strcpy(msgBuf, "No msg yet."); // 初始化消息内容
2658    ... // 处理 ret
2659
2660    ... // 程序主循环代码
2661
2662    if (g_sigFlag == 1) {  // 在退出主循环之后,根据g_sigFlag状态再刷新消息内容
2663        ret = strcpy(msgBuf,  "signal SIGINT received.");
2664        ... // 处理 ret
2665    }
2666
2667    return 0;
2668}
2669```
2670
2671**【影响】**
2672
2673在信号处理程序中访问或修改共享对象,可能造成以不一致的状态访问数据。
2674
2675## 禁用rand函数产生用于安全用途的伪随机数
2676
2677**【描述】**
2678C语言标准库rand()函数生成的是伪随机数,所以不能保证其产生的随机数序列质量。根据C11标准,rand()函数产生的随机数范围是`[0, RAND_MAX(0x7FFF)]`,因为范围相对较短,所以这些数字可以被预测。
2679所以禁止使用rand()函数产生的随机数用于安全用途,必须使用安全的随机数产生方式。
2680
2681典型的安全用途场景包括(但不限于)以下几种:
2682
2683- 会话标识SessionID的生成;
2684- 挑战算法中的随机数生成;
2685- 验证码的随机数生成;
2686- 用于密码算法用途(例如用于生成IV、盐值、密钥等)的随机数生成。
2687
2688**【反例】**
2689程序员期望生成一个唯一的不可被猜测的HTTP会话ID,但该ID是通过调用rand()函数产生的数字随机数,它的ID是可猜测的,并且随机性有限。
2690
2691**【影响】**
2692
2693使用rand()函数可能造成可预测的随机数。
2694
2695## 禁止在发布版本中输出对象或函数的地址
2696
2697**【描述】**
2698禁止在发布版本中输出对象或函数的地址,如:将变量或函数的地址输出到客户端、日志、串口中。
2699
2700当攻击者实施高级攻击时,通常需要先获取目标程序中的内存地址(如变量地址、函数地址等),再通过修改指定内存的内容,达到攻击目的。
2701如果程序中主动输出对象或函数的地址,则为攻击者提供了便利条件,可以根据这些地址以及偏移量计算出其他对象或函数的地址,并实施攻击。
2702另外,由于内存地址泄露,也会造成地址空间随机化的保护功能失效。
2703
2704**【反例】**
2705如下代码中,使用%p格式将指针指向的地址记录到日志中。
2706
2707```c
2708int Encode(unsigned char *in, size_t inSize, unsigned char *out, size_t maxSize)
2709{
2710    ...
2711    Log("in=%p, in size=%zu, out=%p, max size=%zu\n", in, inSize, out, maxSize);
2712    ...
2713}
2714```
2715
2716备注:这里仅用%p打印指针作为示例,代码中将指针转换为整数再打印也存在同样的风险。
2717
2718**【正例】**
2719如下代码中,删除打印地址的代码。
2720
2721```c
2722int Encode(unsigned char *in, size_t inSize, unsigned char *out, size_t maxSize)
2723{
2724    ...
2725    Log("in size=%zu, max size=%zu\n", inSize, maxSize);
2726    ...
2727}
2728```
2729
2730**【例外】**
2731当程序崩溃退出时,在记录崩溃的异常信息中可以输出内存地址等信息。
2732
2733**【影响】**
2734
2735内存地址信息泄露,为攻击者实施攻击提供有利信息,可能造成地址空间随机化防护失效。
2736
2737## 禁止代码中包含公网地址
2738
2739**【描述】**
2740
2741代码或脚本中包含用户不可见,不可知的公网地址,可能会引起客户质疑。
2742
2743对产品发布的软件(包含软件包/补丁包)中包含的公网地址(包括公网IP地址、公网URL地址/域名、邮箱地址)要求如下:
27441、禁止包含用户界面不可见、或产品资料未描述的未公开的公网地址。
27452、已公开的公网地址禁止写在代码或者脚本中,可以存储在配置文件或数据库中。
2746
2747对于开源/第三方软件自带的公网地址必须至少满足上述第1条公开性要求。
2748
2749**【例外】**
2750
2751- 对于标准协议中必须指定公网地址的场景可例外,如soap协议中函数的命名空间必须指定的一个组装的公网URL、http页面中包含w3.org网址等。
2752
2753# 内核安全编程
2754
2755## 内核mmap接口实现中,确保对映射起始地址和大小进行合法性校验
2756
2757**【描述】**
2758
2759**说明**:内核 mmap接口中,经常使用remap_pfn_range()函数将设备物理内存映射到用户进程空间。如果映射起始地址等参数由用户态控制并缺少合法性校验,将导致用户态可通过映射读写任意内核地址。如果攻击者精心构造传入参数,甚至可在内核中执行任意代码。
2760
2761**【错误代码示例】**
2762
2763如下代码在使用remap_pfn_range()进行内存映射时,未对用户可控的映射起始地址和空间大小进行合法性校验,可导致内核崩溃或任意代码执行。
2764
2765```c
2766static int incorrect_mmap(struct file *file, struct vm_area_struct *vma)
2767{
2768	unsigned long size;
2769	size = vma->vm_end - vma->vm_start;
2770	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
2771	//错误:未对映射起始地址、空间大小做合法性校验
2772	if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot)) {
2773		err_log("%s, remap_pfn_range fail", __func__);
2774		return EFAULT;
2775	} else {
2776		vma->vm_flags &=  ~VM_IO;
2777	}
2778
2779	return EOK;
2780}
2781```
2782
2783**【正确代码示例】**
2784
2785增加对映射起始地址等参数的合法性校验。
2786
2787```c
2788static int correct_mmap(struct file *file, struct vm_area_struct *vma)
2789{
2790	unsigned long size;
2791	size = vma->vm_end - vma->vm_start;
2792	//修改:添加校验函数,验证映射起始地址、空间大小是否合法
2793	if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size)) {
2794		return EINVAL;
2795	}
2796
2797	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
2798	if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot)) {
2799		err_log( "%s, remap_pfn_range fail ", __func__);
2800		return EFAULT;
2801	} else {
2802		vma->vm_flags &=  ~VM_IO;
2803	}
2804
2805	return EOK;
2806}
2807```
2808
2809## 内核程序中必须使用内核专用函数读写用户态缓冲区
2810
2811**【描述】**
2812
2813用户态与内核态之间进行数据交换时,如果在内核中不加任何校验(如校验地址范围、空指针)而直接引用用户态传入指针,当用户态传入非法指针时,可导致内核崩溃、任意地址读写等问题。因此,应当禁止使用memcpy()、sprintf()等危险函数,而是使用内核提供的专用函数:copy_from_user()、copy_to_user()、put_user()和get_user()来读写用户态缓冲区,这些函数内部添加了入参校验功能。
2814
2815所有禁用函数列表为:memcpy()、bcopy()、memmove()、strcpy()、strncpy()、strcat()、strncat()、sprintf()、vsprintf()、snprintf()、vsnprintf()、sscanf()、vsscanf()。
2816
2817**【错误代码示例】**
2818
2819内核态直接使用用户态传入的buf指针作为snprintf()的参数,当buf为NULL时,可导致内核崩溃。
2820
2821```c
2822ssize_t incorrect_show(struct file *file, char__user *buf, size_t size, loff_t *data)
2823{
2824	// 错误:直接引用用户态传入指针,如果buf为NULL,则空指针异常导致内核崩溃
2825	return snprintf(buf, size, "%ld\n", debug_level);
2826}
2827```
2828
2829**【正确代码示例】**
2830
2831使用copy_to_user()函数代替snprintf()。
2832
2833```c
2834ssize_t correct_show(struct file *file, char __user *buf, size_t size, loff_t *data)
2835{
2836	int ret = 0;
2837	char level_str[MAX_STR_LEN] = {0};
2838	snprintf(level_str, MAX_STR_LEN, "%ld \n", debug_level);
2839	if(strlen(level_str) >= size) {
2840		return EFAULT;
2841	}
2842
2843	// 修改:使用专用函数copy_to_user()将数据写入到用户态buf,并注意防止缓冲区溢出
2844	ret = copy_to_user(buf, level_str, strlen(level_str)+1);
2845	return ret;
2846}
2847```
2848
2849**【错误代码示例】**
2850
2851内核态直接使用用户态传入的指针user_buf作为数据源进行memcpy()操作,当user_buf为NULL时,可导致内核崩溃。
2852
2853```c
2854size_t incorrect_write(struct file  *file, const char __user  *user_buf, size_t count, loff_t  *ppos)
2855{
2856	...
2857	char buf [128] = {0};
2858	int buf_size = 0;
2859	buf_size = min(count, (sizeof(buf)-1));
2860	// 错误:直接引用用户态传入指针,如果user_buf为NULL,则可导致内核崩溃
2861	(void)memcpy(buf, user_buf, buf_size);
2862	...
2863}
2864```
2865
2866**【正确代码示例】**
2867
2868使用copy_from_user()函数代替memcpy()。
2869
2870```c
2871ssize_t correct_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos)
2872{
2873	...
2874	char buf[128] = {0};
2875	int buf_size = 0;
2876
2877	buf_size = min(count, (sizeof(buf)-1));
2878	// 修改:使用专用函数copy_from_user()将数据写入到内核态buf,并注意防止缓冲区溢出
2879	if (copy_from_user(buf, user_buf, buf_size)) {
2880		return EFAULT;
2881	}
2882
2883	...
2884}
2885```
2886
2887## 必须对copy_from_user()拷贝长度进行校验,防止缓冲区溢出
2888
2889**说明**:内核态从用户态拷贝数据时通常使用copy_from_user()函数,如果未对拷贝长度做校验或者校验不当,会造成内核缓冲区溢出,导致内核panic或提权。
2890
2891**【错误代码示例】**
2892
2893未校验拷贝长度。
2894
2895```c
2896static long gser_ioctl(struct file  *fp, unsigned cmd, unsigned long arg)
2897{
2898	char smd_write_buf[GSERIAL_BUF_LEN];
2899	switch (cmd)
2900	{
2901		case GSERIAL_SMD_WRITE:
2902			if (copy_from_user(&smd_write_arg, argp, sizeof(smd_write_arg))) {...}
2903			// 错误:拷贝长度参数smd_write_arg.size由用户输入,未校验
2904			copy_from_user(smd_write_buf, smd_write_arg.buf, smd_write_arg.size);
2905			...
2906	}
2907}
2908```
2909
2910**【正确代码示例】**
2911
2912添加长度校验。
2913
2914```c
2915static long gser_ioctl(struct file *fp, unsigned cmd, unsigned long arg)
2916{
2917	char smd_write_buf[GSERIAL_BUF_LEN];
2918	switch (cmd)
2919	{
2920		case GSERIAL_SMD_WRITE:
2921			if (copy_from_user(&smd_write_arg, argp, sizeof(smd_write_arg))){...}
2922			// 修改:添加校验
2923			if (smd_write_arg.size  >= GSERIAL_BUF_LEN) {......}
2924			copy_from_user(smd_write_buf, smd_write_arg.buf, smd_write_arg.size);
2925 			...
2926	}
2927}
2928```
2929
2930## 必须对copy_to_user()拷贝的数据进行初始化,防止信息泄漏
2931
2932**【描述】**
2933
2934**说明**:内核态使用copy_to_user()向用户态拷贝数据时,当数据未完全初始化(如结构体成员未赋值、字节对齐引起的内存空洞等),会导致栈上指针等敏感信息泄漏。攻击者可利用绕过kaslr等安全机制。
2935
2936**【错误代码示例】**
2937
2938未完全初始化数据结构成员。
2939
2940```c
2941static long rmnet_ctrl_ioctl(struct file *fp, unsigned cmd, unsigned long arg)
2942{
2943	struct ep_info info;
2944	switch (cmd) {
2945		case FRMNET_CTRL_EP_LOOKUP:
2946			info.ph_ep_info.ep_type = DATA_EP_TYPE_HSUSB;
2947			info.ipa_ep_pair.cons_pipe_num = port->ipa_cons_idx;
2948			info.ipa_ep_pair.prod_pipe_num = port->ipa_prod_idx;
2949			// 错误: info结构体有4个成员,未全部赋值
2950			ret = copy_to_user((void __user *)arg, &info, sizeof(info));
2951			...
2952	}
2953}
2954```
2955
2956**【正确代码示例】**
2957
2958全部进行初始化。
2959
2960```c
2961static long rmnet_ctrl_ioctl(struct file *fp, unsigned cmd, unsigned long arg)
2962{
2963	struct ep_info info;
2964	// 修改:使用memset初始化缓冲区,保证不存在因字节对齐或未赋值导致的内存空洞
2965	(void)memset(&info, '0', sizeof(ep_info));
2966	switch (cmd) {
2967		case FRMNET_CTRL_EP_LOOKUP:
2968			info.ph_ep_info.ep_type = DATA_EP_TYPE_HSUSB;
2969			info.ipa_ep_pair.cons_pipe_num = port->ipa_cons_idx;
2970			info.ipa_ep_pair.prod_pipe_num = port->ipa_prod_idx;
2971			ret = copy_to_user((void __user *)arg, &info, sizeof(info));
2972			...
2973	}
2974}
2975```
2976
2977## 禁止在异常处理中使用BUG_ON宏,避免造成内核panic
2978
2979**【描述】**
2980
2981BUG_ON宏会调用内核的panic()函数,打印错误信息并主动崩溃系统,在正常逻辑处理中(如ioctl接口的cmd参数不识别)不应当使系统崩溃,禁止在此类异常处理场景中使用BUG_ON宏,推荐使用WARN_ON宏。
2982
2983**【错误代码示例】**
2984
2985正常流程中使用了BUG_ON宏
2986
2987```c
2988/ * 判断Q6侧设置定时器是否繁忙,1-忙,0-不忙 */
2989static unsigned int is_modem_set_timer_busy(special_timer *smem_ptr)
2990{
2991	int i = 0;
2992	if (smem_ptr == NULL) {
2993		printk(KERN_EMERG"%s:smem_ptr NULL!\n", __FUNCTION__);
2994		// 错误:系统BUG_ON宏打印调用栈后调用panic(),导致内核拒绝服务,不应在正常流程中使用
2995		BUG_ON(1);
2996		return 1;
2997	}
2998
2999	...
3000}
3001```
3002
3003**【正确代码示例】**
3004
3005去掉BUG_ON宏。
3006
3007```c
3008/ * 判断Q6侧设置定时器是否繁忙,1-忙,0-不忙  */
3009static unsigned int is_modem_set_timer_busy(special_timer *smem_ptr)
3010{
3011	int i = 0;
3012	if (smem_ptr == NULL) {
3013		printk(KERN_EMERG"%s:smem_ptr NULL!\n",  __FUNCTION__);
3014		// 修改:去掉BUG_ON调用,或使用WARN_ON
3015		return 1;
3016	}
3017
3018	...
3019}
3020```
3021
3022## 在中断处理程序或持有自旋锁的进程上下文代码中,禁止使用会引起进程休眠的函数
3023
3024**【描述】**
3025
3026系统以进程为调度单位,在中断上下文中,只有更高优先级的中断才能将其打断,系统在中断处理的时候不能进行进程调度。如果中断处理程序处于休眠状态,就会导致内核无法唤醒,从而使得内核处于瘫痪。
3027
3028自旋锁在使用时,抢占是失效的。若自旋锁在锁住以后进入睡眠,由于不能进行处理器抢占,其它进程都将因为不能获得CPU(单核CPU)而停止运行,对外表现为系统将不作任何响应,出现挂死。
3029
3030因此,在中断处理程序或持有自旋锁的进程上下文代码中,应该禁止使用可能会引起休眠(如vmalloc()、msleep()等)、阻塞(如copy_from_user(),copy_to_user()等)或者耗费大量时间(如printk()等)的函数。
3031
3032## 合理使用内核栈,防止内核栈溢出
3033
3034**【描述】**
3035
3036内核栈大小是固定的(一般32位系统为8K,64位系统为16K,因此资源非常宝贵。不合理的使用内核栈,可能会导致栈溢出,造成系统挂死。因此需要做到以下几点:
3037
3038- 在栈上申请内存空间不要超过内核栈大小;
3039- 注意函数的嵌套使用次数;
3040- 不要定义过多的变量。
3041
3042**【错误代码示例】**
3043
3044以下代码中定义的变量过大,导致栈溢出。
3045
3046```c
3047...
3048struct result
3049{
3050	char name[4];
3051	unsigned int a;
3052	unsigned int b;
3053	unsigned int c;
3054	unsigned int d;
3055}; // 结构体result的大小为20字节
3056
3057int foo()
3058{
3059	struct result temp[512];
3060	// 错误: temp数组含有512个元素,总大小为10K,远超内核栈大小
3061	(void)memset(temp, 0, sizeof(result) * 512);
3062	... // use temp do something
3063	return 0;
3064}
3065
3066...
3067```
3068
3069代码中数组temp有512个元素,总共10K大小,远超内核的8K,明显的栈溢出。
3070
3071**【正确代码示例】**
3072
3073使用kmalloc()代替之。
3074
3075```c
3076...
3077struct result
3078{
3079	char name[4];
3080	unsigned int a;
3081	unsigned int b;
3082	unsigned int c;
3083	unsigned int d;
3084}; // 结构体result的大小为20字节
3085
3086int foo()
3087{
3088	struct result  *temp = NULL;
3089	temp = (result *)kmalloc(sizeof(result) * 512, GFP_KERNEL); //修改:使用kmalloc()申请内存
3090	... // check temp is not NULL
3091	(void)memset(temp, 0, sizeof(result)  * 512);
3092	... // use temp do something
3093	... // free temp
3094	return 0;
3095}
3096...
3097```
3098
3099## 临时关闭地址校验机制后,在操作完成后必须及时恢复
3100
3101**【描述】**
3102
3103SMEP安全机制是指禁止内核执行用户空间的代码(PXN是ARM版本的SMEP)。系统调用(如open(),write()等)本来是提供给用户空间程序访问的。默认情况下,这些函数会对传入的参数地址进行校验,如果入参是非用户空间地址则报错。因此,要在内核程序中使用这些系统调用,就必须使参数地址校验功能失效。set_fs()/get_fs()就用来解决该问题。详细说明见如下代码:
3104
3105```c
3106...
3107mmegment_t old_fs;
3108printk("Hello, I'm the module that intends to write message to file.\n");
3109if (file == NULL) {
3110	file = filp_open(MY_FILE, O_RDWR | O_APPEND | O_CREAT, 0664);
3111}
3112
3113if (IS_ERR(file)) {
3114	printk("Error occurred while opening file %s, exiting ...\n", MY_FILE);
3115	return 0;
3116}
3117
3118sprintf(buf, "%s", "The Message.");
3119old_fs = get_fs(); // get_fs()的作用是获取用户空间地址上限值
3120                   // #define get_fs() (current->addr_limit
3121set_fs(KERNEL_DS); // set_fs的作用是将地址空间上限扩大到KERNEL_DS,这样内核代码可以调用系统函数
3122file->f_op->write(file, (char *)buf, sizeof(buf), &file->f_pos); // 内核代码可以调用write()函数
3123set_fs(old_fs); // 使用完后及时恢复原来用户空间地址限制值
3124...
3125```
3126
3127通过上述代码,可以了解到最为关键的就是操作完成后,要及时恢复地址校验功能。否则SMEP/PXN安全机制就会失效,使得许多漏洞的利用变得很容易。
3128
3129**【错误代码示例】**
3130
3131在程序错误处理分支,未通过set_fs()恢复地址校验功能。
3132
3133```c
3134...
3135oldfs = get_fs();
3136set_fs(KERNEL_DS);
3137/* 在时间戳目录下面创建done文件 */
3138fd = sys_open(path, O_CREAT | O_WRONLY, FILE_LIMIT);
3139if (fd < 0) {
3140	BB_PRINT_ERR("sys_mkdir[%s] error, fd is[%d]\n", path, fd);
3141	return; // 错误:在错误处理程序分支未恢复地址校验机制
3142}
3143
3144sys_close(fd);
3145set_fs(oldfs);
3146...
3147```
3148
3149**【正确代码示例】**
3150
3151在错误处理程序中恢复地址校验功能。
3152
3153```c
3154...
3155oldfs = get_fs();
3156set_fs(KERNEL_DS);
3157
3158/* 在时间戳目录下面创建done文件 */
3159fd = sys_open(path, O_CREAT | O_WRONLY, FILE_LIMIT);
3160if (fd < 0) {
3161	BB_PRINT_ERR("sys_mkdir[%s] error, fd is[%d] \n", path, fd);
3162	set_fs(oldfs); // 修改:在错误处理程序分支中恢复地址校验机制
3163	return;
3164}
3165
3166sys_close(fd);
3167set_fs(oldfs);
3168...
3169```
3170
3171