SDB:安全编码检查清单:C 和 C++
简介
本文应该作为开发人员快速检查代码是否存在已知安全问题的清单。
首先,您只需要验证处理来自程序域之外的数据的代码,即直接的用户输入、从非系统文件读取、从网络读取数据、处理二进制数据(如 JPEG 图像)、接收来自数据库/目录/...服务器的结果等。
以较高权限运行的代码,如系统守护进程或 setuid 应用程序,需要特别注意,因为它们对系统安全构成高风险。 这种代码应始终由 SUSE 安全团队审查。
注意: 本文的目的不是完整和描述特殊情况,而是易于使用和快速验证您的代码。
参见
- http://www.suse.de/~thomas/papers/Security-Guidelines.pdf
- http://www.cert.org/secure-coding/
- http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/index.html
C 和 C++ 代码
检查编译器警告
请确保使用至少-Wall -Wmissing-declarations -Wmissing-prototypes -Wredundant-decls -Wshadow -Wstrict-prototypes -Wformat=2作为 gcc 的编译器标志,以获取有关下面介绍的一些潜在错误的通知。
检查:system(),popen()
这两个调用使用 UNIX 命令 shell 来完成其工作。 因此,不受信任的输入将直接导致系统安全漏洞(shell 命令可以使用元字符执行,或者可以添加或更改命令行选项)。 最好避免使用 system(3) 并将其替换为 fork(2) 和 exec(2) 的组合(当然不要执行 shell)。 popen() 也可以替换为更安全的版本。 有关示例实现,请参阅 http://www.suse.de/~thomas/projects/secproglib/index.html(函数s_popen())或HXproc_*来自 libHX(后者已在 opeSUSE 中提供)。 除了这些函数可能导致的安全问题外,它们还具有高开销并会降低代码速度。
当通过 shell(而非直接)执行支持转义序列以执行其他命令的程序时,也可能出现类似的问题。 例如,'!' 允许在 less 中执行 shell 命令,一些邮件客户端 (MUA) 允许转义序列来执行外部程序。 为了避免成为这种功能的受害者,请阅读程序的文档。
检查:strcpy(),strcat(),sprintf(),scanf(),gets(),...
这些函数不检查目标缓冲区的上限,并复制源缓冲区中的所有字节,直到找到 '\0'(ASCII NUL)。 处理此行为的一种典型方法是在复制数据之前为目标缓冲区分配足够的内存空间。 这里还有另一个问题,可以分配过大的缓冲区,导致拒绝服务情况,或者整数溢出可能导致分配过小的缓冲区,从而导致缓冲区溢出 — 稍后会详细介绍。 函数 gets() 无法保证安全,只能替换它。
检查:strncpy(),strncat(),snprintf(),...
为了避免没有数据复制上限参数的问题,为程序员提供了一组新的相等函数。 这里可能导致麻烦的唯一陷阱是参数的错误大小和算术运算的意外结果。 对于第一个问题,请始终确保size 参数表示目标缓冲区的大小,而不是更多(使用源缓冲区的大小并不罕见,但会使上限检查无效)。 第二个问题更棘手。 size 变量应为类型 size_t(这是此类参数的常用类型,它也是可移植的,但最好阅读文档),此外,请确保更改此变量的所有操作都具有相同的类型 — 保持类型安全,不要溢出或变得太大。 例如
size_t len; ... strncpy(dst, src, len - 1); ...
当 len 为 0 时,len - 1 将是一个高值,并且 dst 缓冲区将被溢出。
由于strncat例如,使用起来可能很麻烦,因为
char buf[1234]; size_t len = sizeof(buf); buf[len] = '\0'; strncpy(buf, "Hello", len - 1); strncat(buf, " World", len - 1 - strlen(buf));
BSD 引入了函数,例如strlcpy使用起来更简单,
char buf[1234]; size_t len = sizeof(buf); strlcpy(buf, "Hello", len); strlcat(buf, " World", len);
这些函数在前面提到的 libHX 中作为 HX_strlcat 提供,例如。
检查:malloc() & 等
当您分配内存并且外部参数是大小的一部分时,您应该确保不要分配过多的内存(拒绝服务)通过设置对您的应用程序有意义的上限。 您还应确保不会发生整数溢出或其他算术问题。 这可能会导致堆上的溢出,并允许攻击者执行任意代码。 最好仅使用无符号整数(如类型 size_t),检查上限和下限,并验证使用不受信任的整数的可能操作是否会导致整数溢出。 典型的测试如下所示
size_t size;
if(size*sizeof(XRefEntry)/sizeof(XRefEntry) != size)
{
error(-1, "Invalid 'size' inside xref table.");
}
entries = malloc(size * sizeof(XRefEntry));
...
检查:错误的类型转换
无需转换库函数的返回结果void *;这会使您的代码难以阅读,不会增加任何价值,并且如果您的作用域中没有有效的原型,可能会隐藏一个错误。 请参阅 http://www.cpax.org.uk/prg/writings/casting.php 和 https://c-faq.cn/malloc/mallocnocast.html 。
检查:循环遍历数组/字符串
缓冲区和整数溢出通常发生在循环中,请确保循环也在达到数组或整数变量的上限/下限时停止。
检查:可变参数列表
一种相对较新的问题是所谓的格式字符串错误。 它们发生在允许可变参数列表的函数(如 printf())并且不受信任的数据直接用作格式字符串而不是格式参数时。 请参阅以下示例以使其清晰
- 错误:snprintf(buf, sizeof(buf), user_input); // 可能是预期的:strlcpy(buf, user_input, sizeof(buf));
- 正确:snprintf(buf, sizeof(buf), "%s", user_input);
检查:文件操作
文件处理可能因上下文而异。 每个子部分将涵盖一个上下文。
对于临时文件,应使用 mkstemp()。
创建文件:权限
文件的权限很重要。 不是每个人都应该能够从/向文件读取或写入。 例如,考虑一个包含机密信息的文件,并使用权限 0644 创建。 另一个需要注意的事项是整数表示权限的基础,它是八进制。 因此,644(十进制)与 0644(八进制)不同。 使用 umask() 在代码的开头可以降低创建权限错误文件的风险。 例如:umask(077)
创建文件:所有者
如果您想创建一个文件,并且同一路径中已经存在同名的另一个文件,您应该检查它是否是常规文件以及是否由同一用户拥有,然后再对其进行操作。 这对于以较高权限运行的进程尤其重要。 使用 open() 和 fstat() 获取文件信息,否则您的代码可能会容易受到所谓的竞争条件的影响。
创建文件:符号链接
如果您的代码从一个文件到另一个文件跟踪文件系统上的链接,有时会存在问题。 文件所有者检查在这里也足够了。
具有更高权限的进程
如果您的进程以较高权限运行并使用另一个用户的的文件,建议将 UID 和 GID 更改为该用户,而不是保持较高的权限。 这可以在额外的线程/进程中完成。
检查:进程环境
如果您想将机密信息(如密码)传递给另一个进程,请不要使用进程环境 — 最好使用匿名管道(或类似管道,如套接字)。 根据操作系统,进程环境可以被每个用户或仅具有相同 UID 的用户检查。 另一个需要注意的事实是,环境不可信。 进程的调用者可以将变量更改为任意值。
检查:降低权限
系统调用的顺序很重要。 使用
- initgroups()
- setgid()
- setuid()
并且始终验证返回值。
检查:处理敏感信息
写入磁盘的敏感数据应使用成熟的密码学算法进行加密。 如果信息以明文形式保存(例如在配置文件中),则应仅允许 root 访问该文件,而其他人则不允许。 每当包含明文敏感信息的文件(例如临时文件)从磁盘中删除时,在调用 remove()、unlink() 等之前,始终用随机数据覆盖该文件是一个好主意。 从进程的内存中删除信息有点复杂,因为编译器喜欢优化代码并删除所需的指令。 因此,加密密钥和密码不应保存在堆栈缓冲区中,而应保存在堆缓冲区中,并且在不再使用密钥或密码时应覆盖缓冲区的的内容,例如“memset(buf, 0, buf_len)”。 这对于使用垃圾收集器或/和不可变字符串的语言来说尤其成问题,例如 Java。 在 Java 的情况下,可以使用数组代替字符串。
此外,可以使用 mlock() 关闭交换,并使用 setrlimit() 避免将进程内存写入磁盘。
此解决方案不能保证信息不会以某种方式泄露,但它们相对容易使用,可以为边缘问题提供修复。
Java
Java 容易受到整数溢出(不抛出异常)的影响,并且处理文件不安全。(有关详细信息,请参阅关于 C 和 C++ 的部分。)此外,Java 使用所谓的本地代码,通常用低级编程语言(如 C)编写,因此 Java 也容易受到缓冲区溢出或格式字符串漏洞的影响。在处理 JPEG ICC 配置文件时,已经发生过这类问题。
参见
Perl
请参阅 http://www.linux-knowledge-portal.org/en/content.php?&content/security/secprog8.html
Unix Shell 脚本
请参阅 http://www.linux-knowledge-portal.org/en/content.php?&content/security/secprog7.html
使用密码学
- 使用知名库和最新的算法(例如openssl 和 AES)
- 使用足够大的密钥(RSA >= 2048 位,分组密码 >= 128 位)
- 如果您使用公钥密码学(SSL、PKI),请验证证书
- 当需要密钥、会话 ID、IV 等的熵时,使用 getentropy(3)