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