├── C,C++安全指南.md ├── CONTRIBUTING.md ├── Go安全指南.md ├── JavaScript安全指南.md ├── Java安全指南.md ├── LICENSE ├── Python安全指南.md └── README.md /C,C++安全指南.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 目录 5 | 6 | - [1 通用安全指南](#1) 7 | * [I. C/C++使用错误](#1.1) 8 | + [1.1 不得直接使用无长度限制的字符拷贝函数](#1.1.1) 9 | + [1.2 创建进程类的函数的安全规范](#1.1.2) 10 | + [1.3 尽量减少使用 _alloca 和可变长度数组](#1.1.3) 11 | + [1.4 printf系列参数必须对应](#1.1.4) 12 | + [1.5 防止泄露指针(包括%p)的值](#1.1.5) 13 | + [1.6 不应当把用户可修改的字符串作为printf系列函数的“format”参数](#1.1.6) 14 | + [1.7 对数组delete时需要使用delete[]](#1.1.7) 15 | + [1.8 注意隐式符号转换](#1.1.8) 16 | + [1.9 注意八进制问题](#1.1.9) 17 | * [II. 不推荐的编程习惯](#1.2) 18 | + [2.1 switch中应有default](#1.2.1) 19 | + [2.2 不应当在Debug或错误信息中提供过多内容](#1.2.2) 20 | + [2.3 不应该在客户端代码中硬编码对称加密秘钥](#1.2.3) 21 | + [2.4 返回栈上变量的地址](#1.2.4) 22 | + [2.5 有逻辑联系的数组必须仔细检查](#1.2.5) 23 | + [2.6 避免函数的声明和实现不同](#1.2.6) 24 | + [2.7 检查复制粘贴的重复代码](#1.2.7) 25 | + [2.8 左右一致的重复判断/永远为真或假的判断](#1.2.8) 26 | + [2.9 函数每个分支都应有返回值](#1.2.9) 27 | + [2.10 不得使用栈上未初始化的变量](#1.2.10) 28 | + [2.11 不得直接使用刚分配的未初始化的内存(如realloc)](#1.2.11) 29 | + [2.12 校验内存相关函数的返回值](#1.2.12) 30 | + [2.13 不要在if里面赋值](#1.2.13) 31 | + [2.14 确认if里面的按位操作](#1.2.14) 32 | * [III. 多线程](#1.3) 33 | + [3.1 变量应确保线程安全性](#1.3.1) 34 | + [3.2 注意signal handler导致的条件竞争](#1.3.2) 35 | + [3.3 注意Time-of-check Time-of-use条件竞争](#1.3.3) 36 | * [IV. 加密解密](#1.4) 37 | + [4.1 不得明文存储用户密码等敏感数据](#1.4.1) 38 | + [4.2 内存中的用户密码等敏感数据应该安全抹除](#1.4.2) 39 | + [4.3 rand() 类函数应正确初始化](#1.4.3) 40 | + [4.4 在需要高强度安全加密时不应使用弱PRNG函数](#1.4.4) 41 | + [4.5 自己实现的rand范围不应过小](#1.4.5) 42 | * [V. 文件操作](#1.5) 43 | + [5.1 避免路径穿越问题](#1.5.1) 44 | + [5.2 避免相对路径导致的安全问题](#1.5.2) 45 | + [5.3 文件权限控制](#1.5.3) 46 | * [Ⅵ. 内存操作](#1.6) 47 | + [6.1 防止各种越界写](#1.6.1) 48 | + [6.2 防止任意地址写](#1.6.2) 49 | * [Ⅶ. 数字操作](#1.7) 50 | + [7.1 防止整数溢出](#1.7.1) 51 | + [7.2 防止Off-By-One](#1.7.2) 52 | + [7.3 避免大小端错误](#1.7.3) 53 | + [7.4 检查除以零异常](#1.7.4) 54 | + [7.5 防止数字类型的错误强转](#1.7.5) 55 | + [7.6 比较数据大小时加上最小/最大值的校验](#1.7.6) 56 | * [Ⅷ. 指针操作](#1.8) 57 | + [8.1 检查在pointer上使用sizeof](#1.8.1) 58 | + [8.2 检查直接将数组和0比较的代码](#1.8.2) 59 | + [8.3 不应当向指针赋予写死的地址](#1.8.3) 60 | + [8.4 检查空指针](#1.8.4) 61 | + [8.5 释放完后置空指针](#1.8.5) 62 | + [8.6 防止错误的类型转换](#1.8.6) 63 | + [8.7 智能指针使用安全](#1.8.7) 64 |
65 | 66 | 67 | ## 通用安全指南 68 | 69 | 70 | ### 1 C/C++使用错误 71 | 72 | 73 | #### 1.1 【必须】不得直接使用无长度限制的字符拷贝函数 74 | 75 | 不应直接使用legacy的字符串拷贝、输入函数,如strcpy、strcat、sprintf、wcscpy、mbscpy等,这些函数的特征是:可以输出一长串字符串,而不限制长度。如果环境允许,应当使用其_s安全版本替代,或者使用n版本函数(如:snprintf,vsnprintf)。 76 | 77 | 若使用形如sscanf之类的函数时,在处理字符串输入时应当通过%10s这样的方式来严格限制字符串长度,同时确保字符串末尾有\0。如果环境允许,应当使用_s安全版本。 78 | 79 | 但是注意,虽然MSVC 2015时默认引入结尾为0版本的`snprintf`(行为等同于C99定义的`snprintf`)。但更早期的版本中,MSVC的`snprintf`可能是`_snprintf`的宏。而`_snprintf`是不保证\0结尾的(见本节后半部分)。 80 | 81 | ```c++ 82 | (MSVC) 83 | Beginning with the UCRT in Visual Studio 2015 and Windows 10, snprintf is no longer identical to _snprintf. The snprintf function behavior is now C99 standard compliant. 84 | 85 | 从Visual Studio 2015和Windows 10中的UCRT开始,snprintf不再与_snprintf相同。snprintf函数行为现在符合C99标准。 86 | 87 | 请参考:https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/snprintf-snprintf-snprintf-l-snwprintf-snwprintf-l?redirectedfrom=MSDN&view=vs-2019 88 | ``` 89 | 90 | 因此,在使用n系列拷贝函数时,要确保正确计算缓冲区长度,同时,如果你不确定是否代码在各个编译器下都能确保末尾有0时,建议可以适当增加1字节输入缓冲区,并将其置为\0,以保证输出的字符串结尾一定有\0。 91 | 92 | ```c++ 93 | // Good 94 | char buf[101] = {0}; 95 | snprintf(buf, sizeof(buf) - 1, "foobar ...", ...); 96 | ``` 97 | 98 | 一些需要注意的函数,例如`strncpy`和`_snprintf`是不安全的。 `strncpy`不应当被视为`strcpy`的n系列函数,它只是恰巧与其他n系列函数名字很像而已。`strncpy`在复制时,如果复制的长度超过n,不会在结尾补\0。 99 | 100 | 同样,MSVC `_snprintf`系列函数在超过或等于n时也不会以0结尾。如果后续使用非0结尾的字符串,可能泄露相邻的内容或者导致程序崩溃。 101 | 102 | ```c++ 103 | // Bad 104 | char a[4] = {0}; 105 | _snprintf(a, 4, "%s", "AAAA"); 106 | foo = strlen(a); 107 | ``` 108 | 109 | 上述代码在MSVC中执行后, a[4] == 'A',因此字符串未以0结尾。a的内容是"AAAA",调用`strlen(a)`则会越界访问。因此,正确的操作举例如下: 110 | 111 | ```c++ 112 | // Good 113 | char a[4] = {0}; 114 | _snprintf(a, sizeof(a), "%s", "AAAA"); 115 | a[sizeof(a) - 1] = '\0'; 116 | foo = strlen(a); 117 | ``` 118 | 119 | 在 C++ 中,强烈建议用 `string`、`vector` 等更高封装层次的基础组件代替原始指针和动态数组,对提高代码的可读性和安全性都有很大的帮助。 120 | 121 | 关联漏洞: 122 | 123 | `中风险-信息泄露` 124 | 125 | `低风险-拒绝服务` 126 | 127 | `高风险-缓冲区溢出` 128 | 129 | 130 | #### 1.2 【必须】创建进程类的函数的安全规范 131 | 132 | system、WinExec、CreateProcess、ShellExecute等启动进程类的函数,需要严格检查其参数。 133 | 134 | 启动进程需要加上双引号,错误例子: 135 | 136 | ```c++ 137 | // Bad 138 | WinExec("D:\\program files\\my folder\\foobar.exe", SW_SHOW); 139 | ``` 140 | 141 | 当存在`D:\program files\my.exe`的时候,my.exe会被启动。而foobar.exe不会启动。 142 | 143 | ```c++ 144 | // Good 145 | WinExec("\"D:\\program files\\my folder\\foobar.exe\"", SW_SHOW); 146 | ``` 147 | 148 | 另外,如果启动时从用户输入、环境变量读取组合命令行时,还需要注意是否可能存在命令注入。 149 | 150 | ```c++ 151 | // Bad 152 | std::string cmdline = "calc "; 153 | cmdline += user_input; 154 | system(cmdline.c_str()); 155 | ``` 156 | 157 | 比如,当用户输入`1+1 && ls`时,执行的实际上是calc 1+1和ls 两个命令,导致命令注入。 158 | 159 | 需要检查用户输入是否含有非法数据。 160 | 161 | ```c++ 162 | // Good 163 | std::string cmdline = "ls "; 164 | cmdline += user_input; 165 | 166 | if(cmdline.find_first_not_of("1234567890.+-*/e ") == std::string::npos) 167 | system(cmdline.c_str()); 168 | else 169 | warning(...); 170 | ``` 171 | 172 | 关联漏洞: 173 | 174 | `高风险-代码执行` 175 | 176 | `高风险-权限提升` 177 | 178 | 179 | #### 1.3 【必须】尽量减少使用 _alloca 和可变长度数组 180 | 181 | _alloca 和[可变长度数组](https://zh.wikipedia.org/wiki/%E5%8F%AF%E5%8F%98%E9%95%BF%E6%95%B0%E7%BB%84)使用的内存量在编译期间不可知。尤其是在循环中使用时,根据编译器的实现不同,可能会导致:(1)栈溢出,即拒绝服务; (2)缺少栈内存测试的编译器实现可能导致申请到非栈内存,并导致内存损坏。这在栈比较小的程序上,例如IoT设备固件上影响尤为大。对于 C++,可变长度数组也属于非标准扩展,在代码规范中禁止使用。 182 | 183 | 错误示例: 184 | 185 | ```c++ 186 | // Bad 187 | for (int i = 0; i < 100000; i++) { 188 | char* foo = (char *)_alloca(0x10000); 189 | ..do something with foo ..; 190 | } 191 | 192 | void Foo(int size) { 193 | char msg[size]; // 不可控的栈溢出风险! 194 | } 195 | ``` 196 | 197 | 正确示例: 198 | 199 | ```c++ 200 | // Good 201 | // 改用动态分配的堆内存 202 | for (int i = 0; i < 100000; i++) { 203 | char * foo = (char *)malloc(0x10000); 204 | ..do something with foo ..; 205 | if (foo_is_no_longer_needed) { 206 | free(foo); 207 | foo = NULL; 208 | } 209 | } 210 | 211 | void Foo(int size) { 212 | std::string msg(size, '\0'); // C++ 213 | char* msg = malloc(size); // C 214 | } 215 | ``` 216 | 217 | 关联漏洞: 218 | 219 | `低风险-拒绝服务` 220 | 221 | `高风险-内存破坏` 222 | 223 | 224 | #### 1.4 【必须】printf系列参数必须对应 225 | 226 | 所有printf系列函数,如sprintf,snprintf,vprintf等必须对应控制符号和参数。 227 | 228 | 错误示例: 229 | 230 | ```c++ 231 | // Bad 232 | const int buf_size = 1000; 233 | char buffer_send_to_remote_client[buf_size] = {0}; 234 | 235 | snprintf(buffer_send_to_remote_client, buf_size, "%d: %p", id, some_string); // %p 应为 %s 236 | 237 | buffer_send_to_remote_client[buf_size - 1] = '\0'; 238 | send_to_remote(buffer_send_to_remote_client); 239 | ``` 240 | 241 | 正确示例: 242 | 243 | ```c++ 244 | // Good 245 | const int buf_size = 1000; 246 | char buffer_send_to_remote_client[buf_size] = {0}; 247 | 248 | snprintf(buffer_send_to_remote_client, buf_size, "%d: %s", id, some_string); 249 | 250 | buffer_send_to_remote_client[buf_size - 1] = '\0'; 251 | send_to_remote(buffer_send_to_remote_client); 252 | ``` 253 | 254 | 前者可能会让client的攻击者获取部分服务器的原始指针地址,可以用于破坏ASLR保护。 255 | 256 | 关联漏洞: 257 | 258 | `中风险-信息泄露` 259 | 260 | 261 | #### 1.5 【必须】防止泄露指针(包括%p)的值 262 | 263 | 所有printf系列函数,要防止格式化完的字符串泄露程序布局信息。例如,如果将带有%p的字符串泄露给程序,则可能会破坏ASLR的防护效果。使得攻击者更容易攻破程序。 264 | 265 | %p的值只应当在程序内使用,而不应当输出到外部或被外部以某种方式获取。 266 | 267 | 错误示例: 268 | 269 | ```c++ 270 | // Bad 271 | // 如果这是暴露给客户的一个API: 272 | uint64_t GetUniqueObjectId(const Foo* pobject) { 273 | return (uint64_t)pobject; 274 | } 275 | ``` 276 | 277 | 正确示例: 278 | 279 | ```c++ 280 | // Good 281 | uint64_t g_object_id = 0; 282 | 283 | void Foo::Foo() { 284 | this->object_id_ = g_object_id++; 285 | } 286 | 287 | // 如果这是暴露给客户的一个API: 288 | uint64_t GetUniqueObjectId(const Foo* object) { 289 | if (object) 290 | return object->object_id_; 291 | else 292 | error(...); 293 | } 294 | ``` 295 | 296 | 关联漏洞: 297 | 298 | `中风险-信息泄露` 299 | 300 | 301 | #### 1.6 【必须】不应当把用户可修改的字符串作为printf系列函数的“format”参数 302 | 303 | 如果用户可以控制字符串,则通过 %n %p 等内容,最坏情况下可以直接执行任意恶意代码。 304 | 305 | 在以下情况尤其需要注意: WIFI名,设备名…… 306 | 307 | 错误: 308 | 309 | ```c++ 310 | snprintf(buf, sizeof(buf), wifi_name); 311 | ``` 312 | 313 | 正确: 314 | 315 | ```c++ 316 | snprinf(buf, sizeof(buf), "%s", wifi_name); 317 | ``` 318 | 319 | 关联漏洞: 320 | 321 | `高风险-代码执行` 322 | 323 | `高风险-内存破坏` 324 | 325 | `中风险-信息泄露` 326 | 327 | `低风险-拒绝服务` 328 | 329 | 330 | #### 1.7 【必须】对数组delete时需要使用delete[] 331 | 332 | delete []操作符用于删除数组。delete操作符用于删除非数组对象。它们分别调用operator delete[]和operator delete。 333 | 334 | ```c++ 335 | // Bad 336 | Foo* b = new Foo[5]; 337 | delete b; // trigger assert in DEBUG mode 338 | ``` 339 | 340 | 在new[]返回的指针上调用delete将是取决于编译器的未定义行为。代码中存在对未定义行为的依赖是错误的。 341 | 342 | ```c++ 343 | // Good 344 | Foo* b = new Foo[5]; 345 | delete[] b; 346 | ``` 347 | 348 | 在 C++ 代码中,使用 `string`、`vector`、智能指针(比如[std::unique_ptr](https://zh.cppreference.com/w/cpp/memory/unique_ptr))等可以消除绝大多数 `delete[]` 的使用场景,并且代码更清晰。 349 | 350 | 关联漏洞: 351 | 352 | `高风险-内存破坏` 353 | 354 | `中风险-逻辑漏洞` 355 | 356 | `低风险-内存泄漏` 357 | 358 | `低风险-拒绝服务` 359 | 360 | 361 | #### 1.8【必须】注意隐式符号转换 362 | 363 | 两个无符号数相减为负数时,结果应当为一个很大的无符号数,但是小于int的无符号数在运算时可能会有预期外的隐式符号转换。 364 | 365 | ```c++ 366 | // 1 367 | unsigned char a = 1; 368 | unsigned char b = 2; 369 | 370 | if (a - b < 0) // a - b = -1 (signed int) 371 | a = 6; 372 | else 373 | a = 8; 374 | 375 | // 2 376 | unsigned char a = 1; 377 | unsigned short b = 2; 378 | 379 | if (a - b < 0) // a - b = -1 (signed int) 380 | a = 6; 381 | else 382 | a = 8; 383 | ``` 384 | 385 | 上述结果均为a=6 386 | 387 | ```c++ 388 | // 3 389 | unsigned int a = 1; 390 | unsigned short b = 2; 391 | 392 | if (a - b < 0) // a - b = 0xffffffff (unsigned int) 393 | a = 6; 394 | else 395 | a = 8; 396 | 397 | // 4 398 | unsigned int a = 1; 399 | unsigned int b = 2; 400 | 401 | if (a - b < 0) // a - b = 0xffffffff (unsigned int) 402 | a = 6; 403 | else 404 | a = 8; 405 | ``` 406 | 407 | 上述结果均为a=8 408 | 409 | 如果预期为8,则错误代码: 410 | 411 | ```c++ 412 | // Bad 413 | unsigned short a = 1; 414 | unsigned short b = 2; 415 | 416 | if (a - b < 0) // a - b = -1 (signed int) 417 | a = 6; 418 | else 419 | a = 8; 420 | ``` 421 | 422 | 正确代码: 423 | 424 | ```c++ 425 | // Good 426 | unsigned short a = 1; 427 | unsigned short b = 2; 428 | 429 | if ((unsigned int)a - (unsigned int)b < 0) // a - b = 0xffff (unsigned short) 430 | a = 6; 431 | else 432 | a = 8; 433 | ``` 434 | 435 | 关联漏洞: 436 | 437 | `中风险-逻辑漏洞` 438 | 439 | 440 | #### 1.9【必须】注意八进制问题 441 | 442 | 代码对齐时应当使用空格或者编辑器自带的对齐功能,谨慎在数字前使用0来对齐代码,以免不当将某些内容转换为八进制。 443 | 444 | 例如,如果预期为20字节长度的缓冲区,则下列代码存在错误。buf2为020(OCT)长度,实际只有16(DEC)长度,在memcpy后越界: 445 | 446 | ```c++ 447 | // Bad 448 | char buf1[1024] = {0}; 449 | char buf2[0020] = {0}; 450 | 451 | memcpy(buf2, somebuf, 19); 452 | ``` 453 | 454 | 应当在使用8进制时明确注明这是八进制。 455 | 456 | ```c++ 457 | // Good 458 | int access_mask = 0777; // oct, rwxrwxrwx 459 | ``` 460 | 461 | 关联漏洞: 462 | 463 | `中风险-逻辑漏洞` 464 | 465 | 466 | ### 2 不推荐的编程习惯 467 | 468 | 469 | #### 2.1 【必须】switch中应有default 470 | 471 | switch中应该有default,以处理各种预期外的情况。这可以确保switch接受用户输入,或者后期在其他开发者修改函数后确保switch仍可以覆盖到所有情况,并确保逻辑正常运行。 472 | 473 | ```c++ 474 | // Bad 475 | int Foo(int bar) { 476 | switch (bar & 7) { 477 | case 0: 478 | return Foobar(bar); 479 | break; 480 | case 1: 481 | return Foobar(bar * 2); 482 | break; 483 | } 484 | } 485 | ``` 486 | 487 | 例如上述代码switch的取值可能从0~7,所以应当有default: 488 | 489 | ```c++ 490 | // Good 491 | int Foo(int bar) { 492 | switch (bar & 7) { 493 | case 0: 494 | return Foobar(bar); 495 | break; 496 | case 1: 497 | return Foobar(bar * 2); 498 | break; 499 | default: 500 | return -1; 501 | } 502 | } 503 | ``` 504 | 505 | 关联漏洞: 506 | 507 | `中风险-逻辑漏洞` 508 | 509 | `中风险-内存泄漏` 510 | 511 | 512 | #### 2.2 【必须】不应当在Debug或错误信息中提供过多内容 513 | 514 | 包含过多信息的Debug消息不应当被用户获取到。Debug信息可能会泄露一些值,例如内存数据、内存地址等内容,这些内容可以帮助攻击者在初步控制程序后,更容易地攻击程序。 515 | 516 | ```c++ 517 | // Bad 518 | int Foo(int* bar) { 519 | if (bar && *bar == 5) { 520 | OutputDebugInfoToUser("Wrong value for bar %p = %d\n", bar, *bar); 521 | } 522 | } 523 | ``` 524 | 525 | 而应该: 526 | 527 | ```c++ 528 | // Good 529 | int foo(int* bar) { 530 | 531 | #ifdef DEBUG 532 | if (bar && *bar == 5) { 533 | OutputDebugInfo("Wrong value for bar.\n"); 534 | } 535 | #endif 536 | 537 | } 538 | ``` 539 | 540 | 关联漏洞: 541 | 542 | `中风险-信息泄漏` 543 | 544 | 545 | #### 2.3 【必须】不应该在客户端代码中硬编码对称加密秘钥 546 | 547 | 不应该在客户端代码中硬编码对称加密秘钥。例如:不应在客户端代码使用硬编码的 AES/ChaCha20-Poly1305/SM1 密钥,使用固定密钥的程序基本和没有加密一样。 548 | 549 | 如果业务需求是认证加密数据传输,应优先考虑直接用 HTTPS 协议。 550 | 551 | 如果是其它业务需求,可考虑由服务器端生成对称秘钥,客户端通过 HTTPS 等认证加密通信渠道从服务器拉取。 552 | 553 | 或者根据用户特定的会话信息,比如登录认证过程可以根据用户名用户密码业务上下文等信息,使用 HKDF 等算法衍生出对称秘钥。 554 | 555 | 又或者使用 RSA/ECDSA + ECDHE 等进行认证秘钥协商,生成对称秘钥。 556 | 557 | 558 | ```c++ 559 | // Bad 560 | char g_aes_key[] = {...}; 561 | 562 | void Foo() { 563 | .... 564 | AES_func(g_aes_key, input_data, output_data); 565 | } 566 | ``` 567 | 568 | 可以考虑在线为每个用户获取不同的密钥: 569 | 570 | ```c++ 571 | // Good 572 | char* g_aes_key; 573 | 574 | void Foo() { 575 | .... 576 | AES_encrypt(g_aes_key, input_data, output_data); 577 | } 578 | 579 | void Init() { 580 | g_aes_key = get_key_from_https(user_id, ...); 581 | } 582 | ``` 583 | 584 | 关联漏洞: 585 | 586 | `中风险-信息泄露` 587 | 588 | 589 | #### 2.4 【必须】返回栈上变量的地址 590 | 591 | 函数不可以返回栈上的变量的地址,其内容在函数返回后就会失效。 592 | 593 | ```c++ 594 | // Bad 595 | char* Foo(char* sz, int len){ 596 | char a[300] = {0}; 597 | if (len > 100) { 598 | memcpy(a, sz, 100); 599 | } 600 | a[len] = '\0'; 601 | return a; // WRONG 602 | } 603 | ``` 604 | 605 | 而应当使用堆来传递非简单类型变量。 606 | 607 | ```c++ 608 | // Good 609 | char* Foo(char* sz, int len) { 610 | char* a = new char[300]; 611 | if (len > 100) { 612 | memcpy(a, sz, 100); 613 | } 614 | a[len] = '\0'; 615 | return a; // OK 616 | } 617 | ``` 618 | 619 | 对于 C++ 程序来说,强烈建议返回 `string`、`vector` 等类型,会让代码更加简单和安全。 620 | 621 | 关联漏洞: 622 | 623 | `高风险-内存破坏` 624 | 625 | 626 | #### 2.5 【必须】有逻辑联系的数组必须仔细检查 627 | 628 | 例如下列程序将字符串转换为week day,但是两个数组并不一样长,导致程序可能会越界读一个int。 629 | 630 | ```c++ 631 | // Bad 632 | int nWeekdays[] = {1, 2, 3, 4, 5, 6}; 633 | const char* sWeekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 634 | for (int x = 0; x < ARRAY_SIZE(sWeekdays); x++) { 635 | if (strcmp(sWeekdays[x], input) == 0) 636 | return nWeekdays[x]; 637 | } 638 | ``` 639 | 640 | 应当确保有关联的nWeekdays和sWeekdays数据统一。 641 | 642 | ```c++ 643 | // Good 644 | const int nWeekdays[] = {1, 2, 3, 4, 5, 6, 7}; 645 | const char* sWeekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; 646 | assert(ARRAY_SIZE(nWeekdays) == ARRAY_SIZE(sWeekdays)); 647 | for (int x = 0; x < ARRAY_SIZE(sWeekdays); x++) { 648 | if (strcmp(sWeekdays[x], input) == 0) { 649 | return nWeekdays[x]; 650 | } 651 | } 652 | ``` 653 | 654 | 关联漏洞: 655 | 656 | `高风险-内存破坏` 657 | 658 | 659 | #### 2.6 【必须】避免函数的声明和实现不同 660 | 661 | 在头文件、源代码、文档中列举的函数声明应当一致,不应当出现定义内容错位的情况。 662 | 663 | 错误: 664 | 665 | foo.h 666 | 667 | ```c++ 668 | int CalcArea(int width, int height); 669 | ``` 670 | 671 | foo.cc 672 | 673 | ```c++ 674 | int CalcArea(int height, int width) { // Different from foo.h 675 | if (height > real_height) { 676 | return 0; 677 | } 678 | return height * width; 679 | } 680 | ``` 681 | 682 | 正确: 683 | foo.h 684 | 685 | ```c++ 686 | int CalcArea(int height, int width); 687 | ``` 688 | 689 | foo.cc 690 | 691 | ```c++ 692 | int CalcArea (int height, int width) { 693 | if (height > real_height) { 694 | return 0; 695 | } 696 | return height * width; 697 | } 698 | ``` 699 | 700 | 关联漏洞: 701 | 702 | `中风险-逻辑问题` 703 | 704 | 705 | #### 2.7 【必须】检查复制粘贴的重复代码(相同代码通常代表错误) 706 | 707 | 当开发中遇到较长的句子时,如果你选择了复制粘贴语句,请记得检查每一行代码,不要出现上下两句一模一样的情况,这通常代表代码哪里出现了错误: 708 | 709 | ```c++ 710 | // Bad 711 | void Foobar(SomeStruct& foobase, SomeStruct& foo1, SomeStruct& foo2) { 712 | foo1.bar = (foo1.bar & 0xffff) | (foobase.base & 0xffff0000); 713 | foo1.bar = (foo1.bar & 0xffff) | (foobase.base & 0xffff0000); 714 | } 715 | ``` 716 | 717 | 如上例,通常可能是: 718 | 719 | ```c++ 720 | // Good 721 | void Foobar(SomeStruct& foobase, SomeStruct& foo1, SomeStruct& foo2) { 722 | foo1.bar = (foo1.bar & 0xffff) | (foobase.base & 0xffff0000); 723 | foo2.bar = (foo2.bar & 0xffff) | (foobase.base & 0xffff0000); 724 | } 725 | 726 | ``` 727 | 728 | 最好是把重复的代码片段提取成函数,如果函数比较短,可以考虑定义为 `inline` 函数,在减少冗余的同时也能确保不会影响性能。 729 | 730 | 关联漏洞: 731 | 732 | `中风险-逻辑问题` 733 | 734 | 735 | #### 2.8 【必须】左右一致的重复判断/永远为真或假的判断(通常代表错误) 736 | 737 | 这通常是由于自动完成或例如Visual Assistant X之类的补全插件导致的问题。 738 | 739 | ```c++ 740 | // Bad 741 | if (foo1.bar == foo1.bar) { 742 | … 743 | } 744 | ``` 745 | 746 | 可能是: 747 | 748 | ```c++ 749 | // Good 750 | if (foo1.bar == foo2.bar) { 751 | … 752 | } 753 | ``` 754 | 755 | 关联漏洞: 756 | 757 | `中风险-逻辑问题` 758 | 759 | 760 | #### 2.9 【必须】函数每个分支都应有返回值 761 | 762 | 函数的每个分支都应该有返回值,否则如果函数走到无返回值的分支,其结果是未知的。 763 | 764 | ```c++ 765 | // Bad 766 | int Foo(int bar) { 767 | if (bar > 100) { 768 | return 10; 769 | } else if (bar > 10) { 770 | return 1; 771 | } 772 | } 773 | ``` 774 | 775 | 上述例子当bar<10时,其结果是未知的值。 776 | 777 | ```c++ 778 | // Good 779 | int Foo(int bar) { 780 | if (bar > 100) { 781 | return 10; 782 | } else if (bar > 10) { 783 | return 1; 784 | } 785 | return 0; 786 | } 787 | ``` 788 | 789 | 开启适当级别的警告(GCC 中为 `-Wreturn-type` 并已包含在 `-Wall` 中)并设置为错误,可以在编译阶段发现这类错误。 790 | 791 | 关联漏洞: 792 | 793 | `中风险-逻辑问题` 794 | 795 | `中风险-信息泄漏` 796 | 797 | 798 | #### 2.10 【必须】不得使用栈上未初始化的变量 799 | 800 | 在栈上声明的变量要注意是否在使用它之前已经初始化了 801 | 802 | ```c++ 803 | // Bad 804 | void Foo() { 805 | int foo; 806 | if (Bar()) { 807 | foo = 1; 808 | } 809 | Foobar(foo); // foo可能没有初始化 810 | } 811 | ``` 812 | 813 | 最好在声明的时候就立刻初始化变量,或者确保每个分支都初始化它。开启相应的编译器警告(GCC 中为 `-Wuninitialized`),并把设置为错误级别,可以在编译阶段发现这类错误。 814 | 815 | ```c++ 816 | // Good 817 | void Foo() { 818 | int foo = 0; 819 | if (Bar()) { 820 | foo = 1; 821 | } 822 | Foobar(foo); 823 | } 824 | ``` 825 | 826 | 关联漏洞: 827 | 828 | `中风险-逻辑问题` 829 | 830 | `中风险-信息泄漏` 831 | 832 | 833 | #### 2.11 【建议】不得直接使用刚分配的未初始化的内存(如realloc) 834 | 835 | 一些刚申请的内存通常是直接从堆上分配的,可能包含有旧数据的,直接使用它们而不初始化,可能会导致安全问题。例如,CVE-2019-13751。应确保初始化变量,或者确保未初始化的值不会泄露给用户。 836 | 837 | ```c++ 838 | // Bad 839 | char* Foo() { 840 | char* a = new char[100]; 841 | a[99] = '\0'; 842 | memcpy(a, "char", 4); 843 | return a; 844 | } 845 | ``` 846 | 847 | ```c++ 848 | // Good 849 | char* Foo() { 850 | char* a = new char[100]; 851 | memcpy(a, "char", 4); 852 | a[4] = '\0'; 853 | return a; 854 | } 855 | ``` 856 | 857 | 在 C++ 中,再次强烈推荐用 `string`、`vector` 代替手动内存分配。 858 | 859 | 关联漏洞: 860 | 861 | `中风险-逻辑问题` 862 | 863 | `中风险-信息泄漏` 864 | 865 | 866 | #### 2.12 【必须】校验内存相关函数的返回值 867 | 868 | 与内存分配相关的函数需要检查其返回值是否正确,以防导致程序崩溃或逻辑错误。 869 | 870 | ```c++ 871 | // Bad 872 | void Foo() { 873 | char* bar = mmap(0, 0x800000, .....); 874 | *(bar + 0x400000) = '\x88'; // Wrong 875 | } 876 | 877 | ``` 878 | 879 | 如上例mmap如果失败,bar的值将是0xffffffff (ffffffff),第二行将会往0x3ffffff写入字符,导致越界写。 880 | 881 | ```c++ 882 | // Good 883 | void Foo() { 884 | char* bar = mmap(0, 0x800000, .....); 885 | if(bar == MAP_FAILED) { 886 | return; 887 | } 888 | 889 | *(bar + 0x400000) = '\x88'; 890 | } 891 | ``` 892 | 893 | 关联漏洞: 894 | 895 | `中风险-逻辑问题` 896 | 897 | `高风险-越界操作` 898 | 899 | 900 | #### 2.13 【必须】不要在if里面赋值 901 | 902 | if里赋值通常代表代码存在错误。 903 | 904 | ```c++ 905 | // Bad 906 | void Foo() { 907 | if (bar = 0x99) ... 908 | } 909 | ``` 910 | 911 | 通常应该是: 912 | 913 | ```c++ 914 | // Good 915 | void Foo() { 916 | if (bar == 0x99) ... 917 | } 918 | ``` 919 | 920 | 建议在构建系统中开启足够的编译器警告(GCC 中为 `-Wparentheses` 并已包含在 `-Wall` 中),并把该警告设置为错误。 921 | 922 | 关联漏洞: 923 | 924 | `中风险-逻辑问题` 925 | 926 | 927 | #### 2.14 【建议】确认if里面的按位操作 928 | 929 | if里,非bool类型和非bool类型的按位操作可能代表代码存在错误。 930 | 931 | ```c++ 932 | // Bad 933 | void Foo() { 934 | int bar = 0x1; // binary 01 935 | int foobar = 0x2; // binary 10 936 | 937 | if (foobar & bar) // result = 00, false 938 | ... 939 | } 940 | ``` 941 | 942 | 上述代码可能应该是: 943 | 944 | ```c++ 945 | // Good 946 | void foo() { 947 | int bar = 0x1; 948 | int foobar = 0x2; 949 | 950 | if (foobar && bar) // result : true 951 | ... 952 | } 953 | ``` 954 | 955 | 关联漏洞: 956 | 957 | `中风险-逻辑问题` 958 | 959 | 960 | ### 3 多线程 961 | 962 | 963 | #### 3.1 【必须】变量应确保线程安全性 964 | 965 | 当一个变量可能被多个线程使用时,应当使用原子操作或加锁操作。 966 | 967 | ```c++ 968 | // Bad 969 | char g_somechar; 970 | void foo_thread1() { 971 | g_somechar += 3; 972 | } 973 | 974 | void foo_thread2() { 975 | g_somechar += 1; 976 | } 977 | ``` 978 | 979 | 对于可以使用原子操作的,应当使用一些可以确保内存安全的操作,如: 980 | 981 | ```c++ 982 | // Good 983 | volatile char g_somechar; 984 | void foo_thread1() { 985 | __sync_fetch_and_add(&g_somechar, 3); 986 | } 987 | 988 | void foo_thread2() { 989 | __sync_fetch_and_add(&g_somechar, 1); 990 | } 991 | ``` 992 | 993 | 对于 C 代码,`C11` 后推荐使用 [atomic](https://en.cppreference.com/w/c/atomic) 标准库。 994 | 对于 C++代码,`C++11` 后,推荐使用 [`std::atomic`](https://zh.cppreference.com/w/cpp/atomic/atomic)。 995 | 996 | 关联漏洞: 997 | 998 | `高风险-内存破坏` 999 | 1000 | `中风险-逻辑问题` 1001 | 1002 | 1003 | #### 3.2 【必须】注意signal handler导致的条件竞争 1004 | 1005 | 竞争条件经常出现在信号处理程序中,因为信号处理程序支持异步操作。攻击者能够利用信号处理程序争用条件导致软件状态损坏,从而可能导致拒绝服务甚至代码执行。 1006 | 1007 | 1. 当信号处理程序中发生不可重入函数或状态敏感操作时,就会出现这些问题。因为信号处理程序中随时可以被调用。比如,当在信号处理程序中调用`free`时,通常会出现另一个信号争用条件,从而导致双重释放。即使给定指针在释放后设置为`NULL`,在释放内存和将指针设置为`NULL`之间仍然存在竞争的可能。 1008 | 2. 为多个信号设置了相同的信号处理程序,这尤其有问题——因为这意味着信号处理程序本身可能会重新进入。例如,malloc()和free()是不可重入的,因为它们可能使用全局或静态数据结构来管理内存,并且它们被syslog()等看似无害的函数间接使用;这些函数可能会导致内存损坏和代码执行。 1009 | 1010 | ```c++ 1011 | // Bad 1012 | char *log_message; 1013 | 1014 | void Handler(int signum) { 1015 | syslog(LOG_NOTICE, "%s\n", log_m_essage); 1016 | free(log_message); 1017 | sleep(10); 1018 | exit(0); 1019 | } 1020 | 1021 | int main (int argc, char* argv[]) { 1022 | log_message = strdup(argv[1]); 1023 | signal(SIGHUP, Handler); 1024 | signal(SIGTERM, Handler); 1025 | sleep(10); 1026 | } 1027 | ``` 1028 | 1029 | 可以借由下列操作规避问题: 1030 | 1031 | 1. 避免在多个处理函数中共享某些变量。 1032 | 2. 在信号处理程序中使用同步操作。 1033 | 3. 屏蔽不相关的信号,从而提供原子性。 1034 | 4. 避免在信号处理函数中调用不满足[异步信号安全](https://www.man7.org/linux/man-pages/man7/signal-safety.7.html)的函数。 1035 | 1036 | 关联漏洞: 1037 | 1038 | `高风险-内存破坏` 1039 | 1040 | `中风险-逻辑问题` 1041 | 1042 | 1043 | #### 3.3 【建议】注意Time-of-check Time-of-use (TOCTOU) 条件竞争 1044 | 1045 | TOCTOU: 软件在使用某个资源之前检查该资源的状态,但是该资源的状态可以在检查和使用之间更改,从而使检查结果无效。当资源处于这种意外状态时,这可能会导致软件执行错误操作。 1046 | 1047 | 当攻击者可以影响检查和使用之间的资源状态时,此问题可能与安全相关。这可能发生在共享资源(如**文件、内存**,甚至多线程程序中的**变量**)上。在编程时需要注意避免出现TOCTOU问题。 1048 | 1049 | 例如,下面的例子中,该文件可能已经在检查和lstat之间进行了更新,特别是因为printf有延迟。 1050 | 1051 | ```c++ 1052 | struct stat *st; 1053 | 1054 | lstat("...", st); 1055 | 1056 | printf("foo"); 1057 | 1058 | if (st->st_mtimespec == ...) { 1059 | printf("Now updating things\n"); 1060 | UpdateThings(); 1061 | } 1062 | ``` 1063 | 1064 | TOCTOU难以修复,但是有以下缓解方案: 1065 | 1066 | 1. 限制对来自多个进程的文件的交叉操作。 1067 | 2. 如果必须在多个进程或线程之间共享对资源的访问,那么请尝试限制”检查“(CHECK)和”使用“(USE)资源之间的时间量,使他们相距尽量不要太远。这不会从根本上解决问题,但可能会使攻击更难成功。 1068 | 3. 在Use调用之后重新检查资源,以验证是否正确执行了操作。 1069 | 4. 确保一些环境锁定机制能够被用来有效保护资源。但要确保锁定是检查之前进行的,而不是在检查之后进行的,以便检查时的资源与使用时的资源相同。 1070 | 1071 | 关联漏洞: 1072 | 1073 | `高风险-内存破坏` 1074 | 1075 | `中风险-逻辑问题` 1076 | 1077 | 1078 | ### 4 加密解密 1079 | 1080 | 1081 | #### 4.1 【必须】不得明文存储用户密码等敏感数据 1082 | 1083 | 用户密码应该使用 Argon2, scrypt, bcrypt, pbkdf2 等算法做哈希之后再存入存储系统, https://password-hashing.net/ 1084 | 1085 | https://libsodium.gitbook.io/doc/password_hashing/default_phf#example-2-password-storage 1086 | 1087 | 1088 | 用户敏感数据,应该做到传输过程中加密,存储状态下加密 1089 | 传输过程中加密,可以使用 HTTPS 等认证加密通信协议 1090 | 1091 | 存储状态下加密,可以使用 SQLCipher 等类似方案。 1092 | 1093 | 1094 | #### 4.2 【必须】内存中的用户密码等敏感数据应该安全抹除 1095 | 1096 | 例如用户密码等,即使是临时使用,也应在使用完成后应当将内容彻底清空。 1097 | 1098 | 错误: 1099 | 1100 | ```c++ 1101 | #include 1102 | #include 1103 | 1104 | { 1105 | ... 1106 | string user_password(100, '\0'); 1107 | snprintf(&user_password, "password: %s", user_password.size(), password_from_input); 1108 | ... 1109 | } 1110 | ``` 1111 | 1112 | 正确: 1113 | 1114 | ```c++ 1115 | { 1116 | ... 1117 | string user_password(100, '\0'); 1118 | snprintf(&user_password, "password: %s", user_password.size(), password_from_input); 1119 | ... 1120 | OPENSSL_cleanse(&user_password[0], user_password.size()); 1121 | } 1122 | 1123 | ``` 1124 | 1125 | 关联漏洞: 1126 | 1127 | `高风险-敏感信息泄露` 1128 | 1129 | 1130 | #### 4.3 【必须】rand() 类函数应正确初始化 1131 | 1132 | rand类函数的随机性并不高。而且在使用前需要使用srand()来初始化。未初始化的随机数可能导致某些内容可预测。 1133 | 1134 | ```c++ 1135 | // Bad 1136 | int main() { 1137 | int foo = rand(); 1138 | return 0; 1139 | } 1140 | ``` 1141 | 1142 | 上述代码执行完成后,foo的值是固定的。它等效于 `srand(1); rand();`。 1143 | 1144 | ```c++ 1145 | // Good 1146 | 1147 | int main() { 1148 | srand(time(0)); 1149 | int foo = rand(); 1150 | return 0; 1151 | } 1152 | ``` 1153 | 1154 | 关联漏洞: 1155 | 1156 | `高风险-逻辑漏洞` 1157 | 1158 | 1159 | #### 4.4 【必须】在需要高强度安全加密时不应使用弱PRNG函数 1160 | 1161 | 在需要生成 AES/SM1/HMAC 等算法的密钥/IV/Nonce, RSA/ECDSA/ECDH 等算法的私钥,这类需要高安全性的业务场景,必须使用密码学安全的随机数生成器 (Cryptographically Secure PseudoRandom Number Generator (CSPRNG) ), 不得使用 `rand()` 等无密码学安全性保证的普通随机数生成器。 1162 | 1163 | 推荐使用的 CSPRNG 有: 1164 | 1. OpenSSL 中的 `RAND_bytes()` 函数, https://www.openssl.org/docs/man1.1.1/man3/RAND_bytes.html 1165 | 1166 | 1. libsodium 中的 `randombytes_buf()` 函数 1167 | 1168 | 1. Linux kernel 的 `getrandom()` 系统调用, https://man7.org/linux/man-pages/man2/getrandom.2.html . 1169 | 或者读 /dev/urandom 文件, 或者 /dev/random 文件。 1170 | 1171 | 1. Apple IOS 的 `SecRandomCopyBytes()`, https://developer.apple.com/documentation/security/1399291-secrandomcopybytes 1172 | 1173 | 1. Windows 下的 `BCryptGenRandom()`, `CryptGenRandom()`, `RtlGenRandom()` 1174 | 1175 | 1176 | ```c++ 1177 | #include 1178 | #include 1179 | #include 1180 | #include 1181 | 1182 | { 1183 | unsigned char key[16]; 1184 | if (1 != RAND_bytes(&key[0], sizeof(key))) { //... 错误处理 1185 | return -1; 1186 | } 1187 | 1188 | AES_KEY aes_key; 1189 | if (0 != AES_set_encrypt_key(&key[0], sizeof(key) * 8, &aes_key)) { 1190 | // ... 错误处理 1191 | return -1; 1192 | } 1193 | 1194 | ... 1195 | 1196 | OPENSSL_cleanse(&key[0], sizeof(key)); 1197 | } 1198 | 1199 | ``` 1200 | 1201 | 1202 | `rand()`类函数的随机性并不高。敏感操作时,如设计加密算法时,不得使用rand()或者类似的简单线性同余伪随机数生成器来作为随机数发生器。符合该定义的比特序列的特点是,序列中“1”的数量约等于“0”的数量;同理,“01”、“00”、“10”、“11”的数量大致相同,以此类推。 1203 | 1204 | 例如 C 标准库中的 `rand()` 的实现只是简单的[线性同余算法](https://sourceware.org/git/?p=glibc.git;a=blob;f=stdlib/random_r.c;hb=glibc-2.28#l353),生成的伪随机数具有较强的可预测性。 1205 | 1206 | 当需要实现高强度加密,例如涉及通信安全时,不应当使用 `rand()` 作为随机数发生器。 1207 | 1208 | 实际应用中,[ C++11 标准提供的`random_device`保证加密的安全性和随机性](https://docs.microsoft.com/en-us/cpp/standard-library/random-device-class?redirectedfrom=MSDN&view=vs-2019#remarks) 1209 | 但是 [C++ 标准并不保证这一点](https://stackoverflow.com/questions/44867500/is-stdrandom-device-cryptographic-secure)。跨平台的代码可以考虑用 [OpenSSL](https://wiki.openssl.org/index.php/Random_Numbers) 等保证密码学安全的库里的随机数发生器。 1210 | 1211 | 1212 | 1213 | 关联漏洞: 1214 | 1215 | `高风险-敏感数据泄露` 1216 | 1217 | 1218 | #### 4.5 【必须】自己实现的rand范围不应过小 1219 | 1220 | 如果在弱安全场景相关的算法中自己实现了PRNG,请确保rand出来的随机数不会很小或可预测。 1221 | 1222 | ```c++ 1223 | // Bad 1224 | int32_t val = ((state[0] * 1103515245U) + 12345U) & 999999; 1225 | ``` 1226 | 1227 | 上述例子可能想生成0~999999共100万种可能的随机数,但是999999的二进制是11110100001000111111,与&运算后,0位一直是0,所以生成出的范围明显会小于100万种。 1228 | 1229 | ```c++ 1230 | // Good 1231 | int32_t val = ((state[0] * 1103515245U) + 12345U) % 1000000; 1232 | 1233 | // Good 1234 | int32_t val = ((state[0] * 1103515245U) + 12345U) & 0x7fffffff; 1235 | ``` 1236 | 1237 | 关联漏洞: 1238 | 1239 | `高风险-逻辑漏洞` 1240 | 1241 | 1242 | ### 5 文件操作 1243 | 1244 | 1245 | #### 5.1 【必须】避免路径穿越问题 1246 | 1247 | 在进行文件操作时,需要判断外部传入的文件名是否合法,如果文件名中包含 `../` 等特殊字符,则会造成路径穿越,导致任意文件的读写。 1248 | 1249 | 错误: 1250 | 1251 | ```c++ 1252 | void Foo() { 1253 | char file_path[PATH_MAX] = "/home/user/code/"; 1254 | // 如果传入的文件名包含../可导致路径穿越 1255 | // 例如"../file.txt",则可以读取到上层目录的file.txt文件 1256 | char name[20] = "../file.txt"; 1257 | memcpy(file_path + strlen(file_path), name, sizeof(name)); 1258 | int fd = open(file_path, O_RDONLY); 1259 | if (fd != -1) { 1260 | char data[100] = {0}; 1261 | int num = 0; 1262 | memset(data, 0, sizeof(data)); 1263 | num = read(fd, data, sizeof(data)); 1264 | if (num > 0) { 1265 | write(STDOUT_FILENO, data, num); 1266 | } 1267 | close(fd); 1268 | } 1269 | } 1270 | ``` 1271 | 1272 | 正确: 1273 | 1274 | ```c++ 1275 | void Foo() { 1276 | char file_path[PATH_MAX] = "/home/user/code/"; 1277 | char name[20] = "../file.txt"; 1278 | // 判断传入的文件名是否非法,例如"../file.txt"中包含非法字符../,直接返回 1279 | if (strstr(name, "..") != NULL){ 1280 | // 包含非法字符 1281 | return; 1282 | } 1283 | memcpy(file_path + strlen(file_path), name, sizeof(name)); 1284 | int fd = open(file_path, O_RDONLY); 1285 | if (fd != -1) { 1286 | char data[100] = {0}; 1287 | int num = 0; 1288 | memset(data, 0, sizeof(data)); 1289 | num = read(fd, data, sizeof(data)); 1290 | if (num > 0) { 1291 | write(STDOUT_FILENO, data, num); 1292 | } 1293 | close(fd); 1294 | } 1295 | } 1296 | ``` 1297 | 1298 | 关联漏洞: 1299 | 1300 | `高风险-逻辑漏洞` 1301 | 1302 | 1303 | #### 5.2 【必须】避免相对路径导致的安全问题(DLL、EXE劫持等问题) 1304 | 1305 | 在程序中,使用相对路径可能导致一些安全风险,例如DLL、EXE劫持等问题。 1306 | 1307 | 例如以下代码,可能存在劫持问题: 1308 | 1309 | ```c++ 1310 | int Foo() { 1311 | // 传入的是dll文件名,如果当前目录下被写入了恶意的同名dll,则可能导致dll劫持 1312 | HINSTANCE hinst = ::LoadLibrary("dll_nolib.dll"); 1313 | if (hinst != NULL) { 1314 | cout<<"dll loaded!" << endl; 1315 | } 1316 | return 0; 1317 | } 1318 | ``` 1319 | 1320 | 针对DLL劫持的安全编码的规范: 1321 | 1322 | 1)调用LoadLibrary,LoadLibraryEx,CreateProcess,ShellExecute等进行模块加载的函数时,指明模块的完整(全)路径,禁止使用相对路径,这样就可避免从其它目录加载DLL。 1323 | 2)在应用程序的开头调用SetDllDirectory(TEXT("")); 从而将当前目录从DLL的搜索列表中删除。结合SetDefaultDllDirectories,AddDllDirectory,RemoveDllDirectory这几个API配合使用,可以有效的规避DLL劫持问题。这些API只能在打了KB2533623补丁的Windows7,2008上使用。 1324 | 1325 | 关联漏洞: 1326 | 1327 | `中风险-逻辑漏洞` 1328 | 1329 | 1330 | #### 5.3 【必须】文件权限控制 1331 | 1332 | 在创建文件时,需要根据文件的敏感级别设置不同的访问权限,以防止敏感数据被其他恶意程序读取或写入。 1333 | 1334 | 错误: 1335 | 1336 | ```c++ 1337 | int Foo() { 1338 | // 不要设置为777权限,以防止被其他恶意程序操作 1339 | if (creat("file.txt", 0777) < 0) { 1340 | printf("文件创建失败!\n"); 1341 | } else { 1342 | printf("文件创建成功!\n"); 1343 | } 1344 | return 0; 1345 | } 1346 | ``` 1347 | 1348 | 关联漏洞: 1349 | 1350 | `中风险-逻辑漏洞` 1351 | 1352 | 1353 | ### 6 内存操作 1354 | 1355 | 1356 | #### 6.1 【必须】防止各种越界写(向前/向后) 1357 | 1358 | 错误1: 1359 | 1360 | ```c++ 1361 | int a[5]; 1362 | a[5] = 0; 1363 | ``` 1364 | 1365 | 错误2: 1366 | 1367 | ```c++ 1368 | int a[5]; 1369 | int b = user_controlled_value; 1370 | a[b] = 3; 1371 | ``` 1372 | 1373 | 关联漏洞: 1374 | 1375 | `高风险-内存破坏` 1376 | 1377 | 1378 | #### 6.2 【必须】防止任意地址写 1379 | 1380 | 任意地址写会导致严重的安全隐患,可能导致代码执行。因此,在编码时必须校验写入的地址。 1381 | 1382 | 错误: 1383 | 1384 | ```c++ 1385 | void Write(MyStruct dst_struct) { 1386 | char payload[10] = { 0 }; 1387 | memcpy(dst_struct.buf, payload, sizeof(payload)); 1388 | } 1389 | 1390 | int main() { 1391 | MyStruct dst_stuct; 1392 | dst_stuct.buf = (char*)user_controlled_value; 1393 | Write(dst_stuct); 1394 | return 0; 1395 | } 1396 | ``` 1397 | 1398 | 关联漏洞: 1399 | 1400 | `高风险-内存破坏` 1401 | 1402 | 1403 | ### 7 数字操作 1404 | 1405 | 1406 | #### 7.1 【必须】防止整数溢出 1407 | 1408 | 在计算时需要考虑整数溢出的可能,尤其在进行内存操作时,需要对分配、拷贝等大小进行合法校验,防止整数溢出导致的漏洞。 1409 | 1410 | 错误(该例子在计算时产生整数溢出) 1411 | 1412 | ```c++ 1413 | const int kMicLen = 4; 1414 | // 整数溢出 1415 | void Foo() { 1416 | int len = 1; 1417 | char payload[10] = { 0 }; 1418 | char dst[10] = { 0 }; 1419 | // Bad, 由于len小于4,导致计算拷贝长度时,整数溢出 1420 | // len - kMicLen == 0xfffffffd 1421 | memcpy(dst, payload, len - kMicLen); 1422 | } 1423 | ``` 1424 | 1425 | 正确例子 1426 | 1427 | ```c++ 1428 | void Foo() { 1429 | int len = 1; 1430 | char payload[10] = { 0 }; 1431 | char dst[10] = { 0 }; 1432 | int size = len - kMicLen; 1433 | // 拷贝前对长度进行判断 1434 | if (size > 0 && size < 10) { 1435 | memcpy(dst, payload, size); 1436 | printf("memcpy good\n"); 1437 | } 1438 | } 1439 | ``` 1440 | 1441 | 关联漏洞: 1442 | 1443 | `高风险-内存破坏` 1444 | 1445 | 1446 | #### 7.2 【必须】防止Off-By-One 1447 | 1448 | 在进行计算或者操作时,如果使用的最大值或最小值不正确,使得该值比正确值多1或少1,可能导致安全风险。 1449 | 1450 | 错误: 1451 | 1452 | ```c++ 1453 | char firstname[20]; 1454 | char lastname[20]; 1455 | char fullname[40]; 1456 | 1457 | fullname[0] = '\0'; 1458 | 1459 | strncat(fullname, firstname, 20); 1460 | // 第二次调用strncat()可能会追加另外20个字符。如果这20个字符没有终止空字符,则存在安全问题 1461 | strncat(fullname, lastname, 20); 1462 | ``` 1463 | 1464 | 正确: 1465 | 1466 | ```c++ 1467 | char firstname[20]; 1468 | char lastname[20]; 1469 | char fullname[40]; 1470 | 1471 | fullname[0] = '\0'; 1472 | 1473 | // 当使用像strncat()函数时,必须在缓冲区的末尾为终止空字符留下一个空字节,避免off-by-one 1474 | strncat(fullname, firstname, sizeof(fullname) - strlen(fullname) - 1); 1475 | strncat(fullname, lastname, sizeof(fullname) - strlen(fullname) - 1); 1476 | ``` 1477 | 1478 | 对于 C++ 代码,再次强烈建议使用 `string`、`vector` 等组件代替原始指针和数组操作。 1479 | 1480 | 关联漏洞: 1481 | 1482 | `高风险-内存破坏` 1483 | 1484 | 1485 | #### 7.3 【必须】避免大小端错误 1486 | 1487 | 在一些涉及大小端数据处理的场景,需要进行大小端判断,例如从大端设备取出的值,要以大端进行处理,避免端序错误使用。 1488 | 1489 | 关联漏洞: 1490 | 1491 | `中风险-逻辑漏洞` 1492 | 1493 | 1494 | #### 7.4 【必须】检查除以零异常 1495 | 1496 | 在进行除法运算时,需要判断被除数是否为零,以防导致程序不符合预期或者崩溃。 1497 | 1498 | 错误: 1499 | 1500 | ```c++ 1501 | int divide(int x, int y) { 1502 | return x / y; 1503 | } 1504 | ``` 1505 | 1506 | 正确: 1507 | 1508 | ```c++ 1509 | int divide(int x, int y) { 1510 | if (y == 0) { 1511 | throw DivideByZero; 1512 | } 1513 | return x / y; 1514 | } 1515 | ``` 1516 | 1517 | 关联漏洞: 1518 | 1519 | `低风险-拒绝服务` 1520 | 1521 | 1522 | #### 7.5 【必须】防止数字类型的错误强转 1523 | 1524 | 在有符号和无符号数字参与的运算中,需要注意类型强转可能导致的逻辑错误,建议指定参与计算时数字的类型或者统一类型参与计算。 1525 | 1526 | 错误例子 1527 | 1528 | ```c++ 1529 | int Foo() { 1530 | int len = 1; 1531 | unsigned int size = 9; 1532 | // 1 < 9 - 10 ? 由于运算中无符号和有符号混用,导致计算结果以无符号计算 1533 | if (len < size - 10) { 1534 | printf("Bad\n"); 1535 | } else { 1536 | printf("Good\n"); 1537 | } 1538 | } 1539 | ``` 1540 | 1541 | 正确例子 1542 | 1543 | ```c++ 1544 | void Foo() { 1545 | // 统一两者计算类型为有符号 1546 | int len = 1; 1547 | int size = 9; 1548 | if (len < size - 10) { 1549 | printf("Bad\n"); 1550 | } else { 1551 | printf("Good\n"); 1552 | } 1553 | } 1554 | ``` 1555 | 1556 | 关联漏洞: 1557 | 1558 | `高风险-内存破坏` 1559 | 1560 | `中风险-逻辑漏洞` 1561 | 1562 | 1563 | #### 7.6 【必须】比较数据大小时加上最小/最大值的校验 1564 | 1565 | 在进行数据大小比较时,要合理地校验数据的区间范围,建议根据数字类型,对其进行最大和最小值的判断,以防止非预期错误。 1566 | 1567 | 错误: 1568 | 1569 | ```c++ 1570 | void Foo(int index) { 1571 | int a[30] = {0}; 1572 | // 此处index是int型,只考虑了index小于数组大小,但是并未判断是否大于等于0 1573 | if (index < 30) { 1574 | // 如果index为负数,则越界 1575 | a[index] = 1; 1576 | } 1577 | } 1578 | ``` 1579 | 1580 | 正确: 1581 | 1582 | ```c++ 1583 | void Foo(int index) { 1584 | int a[30] = {0}; 1585 | // 判断index的最大最小值 1586 | if (index >= 0 && index < 30) { 1587 | a[index] = 1; 1588 | } 1589 | } 1590 | ``` 1591 | 1592 | 关联漏洞: 1593 | 1594 | `高风险-内存破坏` 1595 | 1596 | 1597 | ### 8 指针操作 1598 | 1599 | 1600 | #### 8.1 【建议】检查在pointer上使用sizeof 1601 | 1602 | 除了测试当前指针长度,否则一般不会在pointer上使用sizeof。 1603 | 1604 | 正确: 1605 | 1606 | ```c++ 1607 | size_t pointer_length = sizeof(void*); 1608 | ``` 1609 | 1610 | 可能错误: 1611 | 1612 | ```c++ 1613 | size_t structure_length = sizeof(Foo*); 1614 | ``` 1615 | 1616 | 可能是: 1617 | 1618 | ```c++ 1619 | size_t structure_length = sizeof(Foo); 1620 | ``` 1621 | 1622 | 关联漏洞: 1623 | 1624 | `中风险-逻辑漏洞` 1625 | 1626 | 1627 | #### 8.2 【必须】检查直接将数组和0比较的代码 1628 | 1629 | 错误: 1630 | 1631 | ```c++ 1632 | int a[3]; 1633 | ...; 1634 | 1635 | if (a > 0) 1636 | ...; 1637 | ``` 1638 | 1639 | 该判断永远为真,等价于: 1640 | 1641 | ```c++ 1642 | int a[3]; 1643 | ...; 1644 | 1645 | if (&a[0]) 1646 | ...; 1647 | ``` 1648 | 1649 | 可能是: 1650 | 1651 | ```c++ 1652 | int a[3]; 1653 | ...; 1654 | 1655 | if(a[0] > 0) 1656 | ...; 1657 | ``` 1658 | 1659 | 开启足够的编译器警告(GCC 中为 `-Waddress`,并已包含在 `-Wall` 中),并设置为错误,可以在编译期间发现该问题。 1660 | 1661 | 关联漏洞: 1662 | 1663 | `中风险-逻辑漏洞` 1664 | 1665 | 1666 | #### 8.3 【必须】不应当向指针赋予写死的地址 1667 | 1668 | 特殊情况需要特殊对待(比如开发硬件固件时可能需要写死) 1669 | 1670 | 但是如果是系统驱动开发之类的,写死可能会导致后续的问题。 1671 | 1672 | 关联漏洞: 1673 | 1674 | `高风险-内存破坏` 1675 | 1676 | 1677 | #### 8.4 【必须】检查空指针 1678 | 1679 | 错误: 1680 | 1681 | ```c++ 1682 | *foo = 100; 1683 | 1684 | if (!foo) { 1685 | ERROR("foobar"); 1686 | } 1687 | ``` 1688 | 1689 | 正确: 1690 | 1691 | ```c++ 1692 | if (!foo) { 1693 | ERROR("foobar"); 1694 | } 1695 | 1696 | *foo = 100; 1697 | ``` 1698 | 1699 | 错误: 1700 | 1701 | ```c++ 1702 | void Foo(char* bar) { 1703 | *bar = '\0'; 1704 | } 1705 | ``` 1706 | 1707 | 正确: 1708 | 1709 | ```c++ 1710 | void Foo(char* bar) { 1711 | if(bar) 1712 | *bar = '\0'; 1713 | else 1714 | ...; 1715 | } 1716 | ``` 1717 | 1718 | 关联漏洞: 1719 | 1720 | `低风险-拒绝服务` 1721 | 1722 | 1723 | #### 8.5 【必须】释放完后置空指针 1724 | 1725 | 在对指针进行释放后,需要将该指针设置为NULL,以防止后续free指针的误用,导致UAF等其他内存破坏问题。尤其是在结构体、类里面存储的原始指针。 1726 | 1727 | 错误: 1728 | 1729 | ```c++ 1730 | void foo() { 1731 | char* p = (char*)malloc(100); 1732 | memcpy(p, "hello", 6); 1733 | printf("%s\n", p); 1734 | free(p); // 此时p所指向的内存已被释放,但是p所指的地址仍然不变 1735 | // 未设置为NULL,可能导致UAF等内存错误 1736 | 1737 | if (p != NULL) { // 没有起到防错作用 1738 | printf("%s\n", p); // 错误使用已经释放的内存 1739 | } 1740 | } 1741 | ``` 1742 | 1743 | 正确: 1744 | 1745 | ```c++ 1746 | void foo() { 1747 | char* p = (char*)malloc(100); 1748 | memcpy(p, "hello", 6); 1749 | // 此时p所指向的内存已被释放,但是p所指的地址仍然不变 1750 | printf("%s\n", p); 1751 | free(p); 1752 | //释放后将指针赋值为空 1753 | p = NULL; 1754 | if (p != NULL) { // 没有起到防错作用 1755 | printf("%s\n", p); // 错误使用已经释放的内存 1756 | } 1757 | } 1758 | ``` 1759 | 1760 | 对于 C++ 代码,使用 string、vector、智能指针等代替原始内存管理机制,可以大量减少这类错误。 1761 | 1762 | 关联漏洞: 1763 | 1764 | `高风险-内存破坏` 1765 | 1766 | 1767 | #### 8.6 【必须】防止错误的类型转换(type confusion) 1768 | 1769 | 在对指针、对象或变量进行操作时,需要能够正确判断所操作对象的原始类型。如果使用了与原始类型不兼容的类型进行访问,则存在安全隐患。 1770 | 1771 | 错误: 1772 | 1773 | ```c++ 1774 | const int NAME_TYPE = 1; 1775 | const int ID_TYPE = 2; 1776 | 1777 | // 该类型根据 msg_type 进行区分,如果在对MessageBuffer进行操作时没有判断目标对象,则存在类型混淆 1778 | struct MessageBuffer { 1779 | int msg_type; 1780 | union { 1781 | const char *name; 1782 | int name_id; 1783 | }; 1784 | }; 1785 | 1786 | void Foo() { 1787 | struct MessageBuffer buf; 1788 | const char* default_message = "Hello World"; 1789 | // 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name 1790 | buf.msg_type = NAME_TYPE; 1791 | buf.name = default_message; 1792 | printf("Pointer of buf.name is %p\n", buf.name); 1793 | 1794 | // 没有判断目标消息类型是否为ID_TYPE,直接修改nameID,导致类型混淆 1795 | buf.name_id = user_controlled_value; 1796 | 1797 | if (buf.msg_type == NAME_TYPE) { 1798 | printf("Pointer of buf.name is now %p\n", buf.name); 1799 | // 以NAME_TYPE作为类型操作,可能导致非法内存读写 1800 | printf("Message: %s\n", buf.name); 1801 | } else { 1802 | printf("Message: Use ID %d\n", buf.name_id); 1803 | } 1804 | } 1805 | ``` 1806 | 1807 | 正确(判断操作的目标是否是预期类型): 1808 | 1809 | ```c++ 1810 | void Foo() { 1811 | struct MessageBuffer buf; 1812 | const char* default_message = "Hello World"; 1813 | // 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name 1814 | buf.msg_type = NAME_TYPE; 1815 | buf.name = default_msessage; 1816 | printf("Pointer of buf.name is %p\n", buf.name); 1817 | 1818 | // 判断目标消息类型是否为 ID_TYPE,不是预期类型则做对应操作 1819 | if (buf.msg_type == ID_TYPE) 1820 | buf.name_id = user_controlled_value; 1821 | 1822 | if (buf.msg_type == NAME_TYPE) { 1823 | printf("Pointer of buf.name is now %p\n", buf.name); 1824 | printf("Message: %s\n", buf.name); 1825 | } else { 1826 | printf("Message: Use ID %d\n", buf.name_id); 1827 | } 1828 | } 1829 | ``` 1830 | 1831 | 关联漏洞: 1832 | 1833 | `高风险-内存破坏` 1834 | 1835 | 1836 | #### 8.7 【必须】智能指针使用安全 1837 | 1838 | 在使用智能指针时,防止其和原始指针的混用,否则可能导致对象生命周期问题,例如 UAF 等安全风险。 1839 | 1840 | 错误例子: 1841 | 1842 | ```c++ 1843 | class Foo { 1844 | public: 1845 | explicit Foo(int num) { data_ = num; }; 1846 | void Function() { printf("Obj is %p, data = %d\n", this, data_); }; 1847 | private: 1848 | int data_; 1849 | }; 1850 | 1851 | std::unique_ptr fool_u_ptr = nullptr; 1852 | Foo* pfool_raw_ptr = nullptr; 1853 | 1854 | void Risk() { 1855 | fool_u_ptr = make_unique(1); 1856 | 1857 | // 从独占智能指针中获取原始指针,(1) 1858 | pfool_raw_ptr = fool_u_ptr.get(); 1859 | // 调用(1)的函数 1860 | pfool_raw_ptr->Function(); 1861 | 1862 | // 独占智能指针重新赋值后会释放内存 1863 | fool_u_ptr = make_unique(2); 1864 | // 通过原始指针操作会导致UAF,pfool_raw_ptr指向的对象已经释放 1865 | pfool_raw_ptr->Function(); 1866 | } 1867 | 1868 | 1869 | // 输出: 1870 | // Obj is 0000027943087B80, data = 1 1871 | // Obj is 0000027943087B80, data = -572662307 1872 | ``` 1873 | 1874 | 正确,通过智能指针操作: 1875 | 1876 | ```c++ 1877 | void Safe() { 1878 | fool_u_ptr = make_unique(1); 1879 | // 调用(1)的函数 1880 | fool_u_ptr->Function(); 1881 | 1882 | fool_u_ptr = make_unique(2); 1883 | // 调用(2)的函数 1884 | fool_u_ptr->Function(); 1885 | } 1886 | 1887 | // 输出: 1888 | // Obj is 000002C7BB550830, data = 1 1889 | // Obj is 000002C7BB557AF0, data = 2 1890 | ``` 1891 | 1892 | 关联漏洞: 1893 | 1894 | `高风险-内存破坏` 1895 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 为 代码安全指南 作出贡献 2 | 3 | 欢迎 [提出问题或建议](issues) 或 [提交合并请求](pulls),建议在为项目作出贡献时,阅读以下指南。 4 | 5 | ### I. Commit Mesage 编写指引 6 | 7 | 为便于索引,Commit Message应包括三个部分:Header(必需),Body(可选)和 Footer(可选)。 8 | 9 | ```html 10 | (): 11 | // 空一行 12 | 13 | // 空一行 14 |