360代码规范一:security
360代码规范一:security
Hoshea ZhangR1.1 敏感数据不可写入代码
ID_plainSensitiveInfo :shield: security warning
代码中的敏感数据极易泄露,产品及相关运维、测试工具的代码均不可记录任何敏感数据。
示例:
1 | /** |
将密码等敏感数据写入代码是非常不安全的,即使例中 Y2Fycm90 是实际密码的某种变换,聪明的读者也会很快将其破解。
敏感数据的界定是产品设计的重要环节。对具有高可靠性要求的客户端软件,不建议保存任何敏感数据,对于必须保存敏感数据的软件系统,则需要落实安全的存储机制以及相关的评审与测试。
相关
ID_secretLeak
参考
CWE-259
CWE-798
SEI CERT MSC41-C
R1.2 敏感数据不可被系统外界感知
ID_secretLeak :shield: security warning
敏感数据出入软件系统时需采用有效的保护措施。
示例:
1 | void foo(User* u) { |
显然,将敏感数据直接输出到界面、日志或其他外界可感知的介质中是不安全的,需避免敏感数据的有意外传,除此之外,还需要落实具体的保护措施。
保护措施包括但不限于:
- 避免用明文或弱加密方式传输敏感数据
- 避免敏感数据从内存交换到外存
- 避免避免敏感数据写入内存转储文件
- 应具备反调试机制,使外界无法获得程序的内部数据
- 应具备反注入机制,使外界无法篡改程序的行为
下面以 Windows 平台为例,给出阻止敏感数据从内存交换到外存的示例:
1 | class SecretBuf { |
例中 SecretBuf 是一个缓冲区类,其申请的内存会被锁定在物理内存中,不会与外存交换,可在一定程度上防止其他进程的恶意嗅探,保障缓冲区内数据的安全。SecretBuf 在构造函数中通过 VirtualLock 锁定物理内存,在析构函数中通过 VirtualUnlock 解除锁定,解锁之前有必要清除数据,否则解锁之后残留数据仍有可能被交换到外存,进一步可参见 ID_unsafeCleanup。
SecretBuf 的使用方法如下:
1 | void foo() { |
在 Linux 等系统中可参见如下有相似功能的接口:
1 | int mlock(const void* addr, size_t len); // In <sys/mman.h> |
相关
ID_unsafeCleanup
参考
CWE-528
CWE-591
SEI CERT MEM06-C
R1.3 敏感数据在使用后应被有效清理
ID_unsafeCleanup :shield: security warning
及时清理不再使用的敏感数据是重要的安全措施,且应保证清理过程不会因为编译器的优化而失效。
程序会反复利用内存,敏感数据可能会残留在未初始化的对象或对象之间的填充数据中,如果被存储到磁盘或传输到网络就会造成敏感信息的泄露,可参见 ID_secretLeak 和 ID_ignorePaddingData 的进一步讨论。
示例:
1 | void foo() { |
示例代码调用 memset 覆盖敏感数据以达到清理目的,然而保存敏感信息的 password 为局部数组且 memset 之后没有再被引用,根据相关标准,编译器可将 memset 过程去掉,使敏感数据没有得到有效清理。C11 提供了 memset_s 函数以避免这种问题,某些平台和库也提供了相关支持,如 SecureZeroMemory、explicit_bzero、OPENSSL_cleanse 等不会被优化掉的函数。
在 C++ 代码中,可用 volatile 限定相关数据以避免编译器的优化,再用 std::fill_n 等方法清理,如:
1 | void foo() { |
相关
ID_secretLeak
ID_ignorePaddingData
依据
ISO/IEC 9899:1999 5.1.2.3(3)
ISO/IEC 9899:2011 5.1.2.3(4)
ISO/IEC 9899:2011 K.3.7.4.1
参考
CWE-14
CWE-226
CWE-244
CWE-733
SEI CERT MSC06-C
R1.4 公共成员或全局对象不应记录敏感数据
ID_sensitiveName :shield: security warning
公共成员、全局对象可被外部代码引用,如果存有敏感数据则可能会被误用或窃取。
示例:
1 | extern string password; // Non-compliant |
至少应将相关成员改为 private:
1 | class A { |
敏感数据最好对引用者完全隐藏,避免被恶意分析、复制或序列化。使数据与接口进一步分离,可参见“Pimpl idiom”等模式。
参考
CWE-766
R1.5 预判用户输入造成的不良后果
ID_hijack :shield: security warning
须对用户输入的脚本、路径、资源请求等信息进行预判,对产生不良后果的输入予以拒绝。
示例:
1 | Result foo() { |
设 userInput 返回用户输入的字符串,sqlQuery 将用户输入替换格式化占位符后执行 SQL 语句,如果用户输入“xxx’ or ‘x’=’x”一类的字符串则相当于执行的是“select * from db where key=’xxx’ or ‘x’=’x’”,一个恒为真的条件使 where 限制失效,造成所有数据被返回,这是一种常见的攻击方式,称为“SQL 注入(SQL injection)”,对于 XPath、XQuery、LDAP 等脚本均需考虑这种问题,应在执行前判断用户输入的安全性。
又如:
1 | string bar() { |
这段代码意在将用户输入的路径限制在 /myhome/mydata 目录下,然而这么做是不安全的,如果用户输入带有“../”这种相对路径,则仍可绕过限制,这也是一种常见的攻击方式,称为“路径遍历(directory traversal)”,应在读取文件之前判断路径的安全性。
注意,“用户输入”不单指人的手工输入,源自环境变量、配置文件以及其他软硬件的输入均在此范围内。
参考
CWE-23
CWE-73
CWE-89
CWE-943
R1.6 对资源设定合理的访问权限
ID_unlimitedAuthority :shield: security warning
对资源设定合理的访问权限,避免为攻击者提供不应拥有的权限或能力。
权限的分类包括但不限于:
- 文件、数据库等资源的读写权限
- 计算、IO 过程的执行权限
- 软硬件资源的占用权限
权限设定是产品设计与实现的重要环节,需落实相关的评审与测试。
示例:
1 |
|
例中 umask 函数开放了所有用户对文件的读写权限,这是很不安全的,进程之间不应直接通过文件通信,应实现安全的接口和交互机制。
由于历史原因,C 语言的 fopen 和 C++ 语言的 fstream 都不能确保文件只能被当前用户访问,C11 提供了 fopen_s,C++17 提供了 std::filesystem::permissions 以填补这方面的需求。
C11 fopen_s 简例:
1 |
|
与 fopen 不同,fopen_s 可以不受 umask 等函数的影响,直接将文件的权限设为当前用户私有,其他用户不可访问,降低了文件被窃取或篡改的风险,是一种更安全的方法。
除此之外,如果需要对资源进行更精细的权限管理,可参见“access control list(ACL)”。
依据
ISO/IEC 9899:2011 K.3.5.2.1(7)
ISO/IEC 14882:2017 30.10.15.26
参考
CWE-266
CWE-732
SEI CERT FIO06-C
R1.7 对用户落实有效的权限管理
ID_improperAuthorization :shield: security warning
需落实有效的权限管理,相关措施包括但不限于:
- 落实授权与认证机制,提供多因素认证
- 遵循最小特权原则,对资源和相关算法设置合理的访问或执行权限
- 避免仅在客户端认证而非服务端认证
- 检查请求是否符合用户的权限设定,拒绝无权限的请求
- 用户放弃某项权限后,应确保相关权限不再生效
- 遵循合理的“认证 - 执行”顺序,避免复杂度攻击或早期放大攻击
- 保证信道完整性,对相关用户进行充分的身份认证,避免中间人攻击
- 验证通信通道的源和目的地,拒绝非预期的请求和应答
- 避免攻击者使用重放攻击等手段绕过身份认证或干扰正常运营
- 避免不恰当地信任反向 DNS(关注 DNS Cache Poisoning)
- 避免过于严格且易触发的账户锁定机制,使攻击者通过锁定账户干扰正常运营
权限管理与安全直接相关,应落实严格的评审、测试以及攻防演练。
示例:
1 | Result foo() { |
设例中 req 对应用户请求,sqlQuery 将请求中的 key 字段替换格式化占位符后执行查询,这个模式存在多种问题,应先判断用户是否具有读取数据库相关字段的权限,而且还应判断 req[“key”] 的值是否安全,详见 ID_hijack。
又如:
1 | void bar(User* user) { |
设例中 read_large_file 读取大型文件,is_admin 进行身份认证,在身份认证之前访问资源使得攻击者不必获取有效账号即可消耗系统资源,从而对系统造成干扰,所以应该在访问资源之前进行身份认证。
参考
CWE-285
CWE-350
R1.8 避免引用危险符号名称
ID_dangerousName :shield: security warning
弱加密、弱哈希、弱随机、不安全的协议等相关库、函数、类、宏、常量等名称不应出现在代码中。
这种危险符号名称主要来自:
- 低质量随机数生成算法,如 srand、rand 等
- 不再适用的哈希算法,如 MD2、MD4、MD5、MD6、RIPEMD 以及 SHA-1 等
- 非加密协议,如 HTTP、FTP 等
- 低版本的传输层安全协议,如 TLSv1.2 之前的版本
- 弱加密算法,如 DES、3DES 等
示例:
1 | #include <openssl/md5.h> // Non-compliant, obsolete hash algorithm |
参考
CWE-326
CWE-327
R1.9 避免使用危险接口
ID_dangerousFunction :shield: security warning
由于历史原因,有些系统接口甚至标准库函数存在缺陷,无法安全使用,也有一些接口的使用条件很苛刻,难以安全使用。
示例:
1 | gets // The most dangerous function |
例中 gets 函数不检查缓冲区边界,无法安全使用;TerminateThread 等 Windows API 强制终止线程,线程持有的资源难以正确释放,极易导致泄漏或死锁等问题,应避免使用这类函数。
参考
CWE-242
CWE-676
R1.10 避免使用已过时的接口
ID_obsoleteFunction :shield: security warning
避免使用在相关标准中已过时的接口,应改用更完善的替代方法以规避风险,提高可移植性。
对于过时的 C++ 标准库接口,本规则特化为 ID_obsoleteStdFunction。
示例:
1 | asctime // Use ‘strftime’ instead |
例中 C89 引入的 ctime、asctime 等函数在 POSIX.1-2008 标准中已过时,应改用 strftime 函数;RegCreateKey 等 16 位 Windows API 在 32 和 64 位平台中不应再被使用。
相关
ID_obsoleteStdFunction
参考
CWE-477
R1.11 禁用不安全的字符串函数
ID_unsafeStringFunction :no_entry: security warning
由于历史原因,C 标准库中的某些字符串函数不执行边界检查,易造成运行时错误和安全漏洞。
这类函数包括:
1 | gets、strcpy、strcat、wcscpy、wcscat、 |
与这类函数相似的函数同样受本规则约束,如下列 Windows API:
1 | StrCpy、StrCpyA、StrCpyW、StrCat、StrCatA、StrCatW、 |
在 C 代码中应采用更安全的库函数,如用 fgets 代替 gets,snprintf 代替 sprintf。在 C++ 代码中应采用 STL 标准库提供的相关功能。
示例:
1 | char buf[100]; |
例中 gets 函数无法检查缓冲区的大小,一旦输入超过了 buf 数组的边界,程序的数据或流程就会遭到破坏,这种情况会被攻击者利用,可参见 ID_bufferOverflow 的进一步说明。如果代码中存在 gets 等函数,可以直接判定程序是有漏洞的。
应改为:
1 | char buf[100]; |
fgets 与 gets 不同,当输入超过缓冲区大小时会被截断,保证缓冲区之外的数据不会被破坏。
又如:
1 | char buf[100]; |
例中 scanf 函数与 gets 函数有相同的问题,可改为:
1 | char buf[100]; |
scanf、sprintf、strcpy 等函数无视缓冲区大小,需要在外部另行实现防止缓冲区溢出的代码,完全依赖于开发者的小心谨慎。历史表明,对人的单方面依赖是不可靠的,改用更安全的方法才是明智的选择。
相关
ID_bufferOverflow
依据
ISO/IEC 9899:2011 Annex K
ISO/IEC 9899:2011 K.3.7
ISO/IEC 9899:2011 K.3.9
参考
CWE-119
CWE-120
CWE-676
MISRA C++ 2008 18-0-5
R1.12 确保字符串以空字符结尾
ID_improperNullTermination :shield: security warning
语言要求字符串以空字符结尾,程序应保证有足够的内存空间安置空字符,否则会破坏程序基本的执行机制,造成严重问题。
空字符指 ‘\0’、L’\0’、u’\0’、U’\0’,分别对应 char*、wchar_t*、char16_t*、char32_t* 等字符串类型。
示例:
1 | void foo(const char* p) { |
例示代码将字符串复制到数组中,转为大写并打印,然而如果 p 所指字符串的长度超过 3,strncpy 不会在数组的结尾安置空字符 ‘\0’,导致 strupr 内存访问越界,程序可能会崩溃,也可能打印出本该隐藏的敏感数据。
应改为:
1 | void foo(const char* p) { |
将所有数组元素初始化为 ‘\0’,调用 strncpy 后如果数组最后一个元素是 ‘\0’,说明输入字符串的长度符合要求,否则可作出相应的异常处理。
相关
ID_unsafeStringFunction
依据
ISO/IEC 9899:1999 7.21.2.4
ISO/IEC 9899:2011 7.24.2.4
参考
CWE-170
R1.13 避免除 0 等计算异常
ID_divideByZero :shield: security error
除 0 等在数学上没有定义的运算、浮点异常、非法指令、段错误等问题称为“计算异常”,意味着严重的底层运行时错误,而且这种异常无法用语言层面的常规方法捕获。
示例:
1 | int foo(int n) { |
整数除 0 往往会使程序崩溃,浮点数除 0 可以产生“Inf”或“NaN”等无效结果,在某些环境中也可以设置浮点异常使程序收到特定信号。
崩溃会使程序异常终止,无法或难以执行必要的善后工作。如果崩溃可由外部输入引起,会被攻击者利用从而迫使程序无法正常工作,具有高可靠性要求的服务类程序更应该注意这一点,可参见“拒绝服务攻击”。对于客户端程序,也要防止攻击者对崩溃产生的“core dump”进行恶意调试,避免泄露敏感数据,总之程序的健壮性与安全性是紧密相关的。
相关
ID_sig_illReturn
依据
ISO/IEC 9899:1999 6.5.5(5)-undefined
ISO/IEC 9899:2011 6.5.5(5)-undefined
ISO/IEC 14882:2003 5.6(4)-undefined
ISO/IEC 14882:2011 5.6(4)-undefined
ISO/IEC 14882:2017 8.6(4)-undefined
参考
CWE-189
CWE-369
C++ Core Guidelines ES.105
R1.14 格式化字符串应为常量
ID_variableFormatString :shield: security warning
出于可读性和安全性的考量,格式化字符串最好直接写成常量字符串的形式。
本规则是 ID_hijack 的特化。
示例:
1 | int a, b, c; |
例中格式化字符串 fmt 是变量,这种方式可读性较差,而且要注意如果 fmt 可受外界影响,则可能被攻击者利用造成不良后果。
应将 fmt 改为常量:
1 | printf("%d %d %d", a, b, c); // Compliant |
相关
ID_hijack
参考
CWE-134
R1.15 与内存空间布局相关的信息不可被外界感知
ID_addressExposure :shield: security warning
函数、对象、缓冲区的地址以及相关内存区域的长度等信息不可被外界感知,否则会成为攻击者的线索。
示例:
1 | int foo(int* p, int n) { |
示例代码将缓冲区的地址和长度输出到日志是不安全的,这种代码多以调试为目的,不应将其编译到产品的正式版本中。
相关
ID_bufferOverflow
参考
CWE-200
R1.16 与网络地址相关的信息不应写入代码
ID_hardcodedIP :shield: security warning
在代码中记录网络地址不利于维护和移植,也容易暴露产品的网络结构,属于安全隐患。
示例:
1 | string host = "10.16.25.93"; // Non-compliant |
应从配置文件中获取地址,并配以加密措施:
1 | MyConf cfg; |
特殊的 IP 地址可不受本规则限制,如:
1 | 0.0.0.0 |
相关
ID_addressExposure
R1.17 选择安全的异常处理方式
ID_deprecatedErrno :shield: security warning
避免使用 errno 和与其相同的模式,应根据实际需求选择通过函数返回值或 C++ 异常机制来处理异常情况。
errno 被设定的位置和被读取的位置相距较远,不遵循固定的静态结构,极易误用,是不安全的异常处理方式,对异常情况的错误处理往往会成为业务漏洞,使攻击者轻易地实现其目的。
示例:
1 | void foo() { |
例中 somecall 执行异常,通过 errno 获取异常信息,但 errno 的值会被 printf 修改,相应的异常处理也失去了意义。
又如:
1 | void bar(const char* s) { |
errno 并不能反映所有异常情况,atoi 等函数与 errno 无关,例中 errno 的值来自函数外部难以预料的位置,相应的异常处理也将是错误的。
参考
C++ Core Guidelines E.28
MISRA C 2004 20.5
MISRA C++ 2008 19-3-1
R1.18 启用平台和编译器提供的防御机制
ID_missingHardening :shield: security suggestion
针对一些常见攻击,平台和编译器会提供防御机制,如:
程序应利用这种机制加强自身的安全性,进一步可参见“security hardening)”。
示例:
1 | // In test.c |
如果在 Linux 等平台上按如下方式编译:
1 | cc test.c -o test |
各函数的地址在虚拟内存中是固定的,易被攻击者猜中,进而施展攻击手段。
当平台启用了“ASLR”机制,再按如下方式编译:
1 | cc test.c -o test -fPIE -pie |
可使程序各结构的地址随机化,函数的地址在每次运行时均不相同,有效提高了攻击难度。
如无特殊原因,在编译程序时不应屏蔽这种防御机制,如:
1 | cc test.c -o test -z execstack # Non-compliant, disable NX |
如果必须屏蔽,应落实相关的评审与测试。