HaoShilao.Net
^
安全编程: 避免竞争条件(4)
共享目录的解决方案

  不幸的是,有很多并不是解决方案。有一些程序只是直接调用 mktemp(3) 或 tmpnam(3) 来创建临时文件,然后基于这样做会成功的假定去简单地打开它。错误的计划!实际上,线程使用 tmpnam(3) 并不可靠,也不能可靠地处理循环,所以永远不要使用它。1997 年的“Single Unix Specification”推荐使用 tmpfile(3),但不幸的是,在一些老系统上实现它很不安全。

  在 C 中,为了在共享(sticky)目录中安全地创建临时文件,通常的解决方案是将 umask() 值设置为一个非常受限制的值,然后反复进行以下操作:(1)创建一个“随机的”文件名,(2)使用 O_CREAT | O_EXCL 选项 open(2) 它(这将自动创建文件,如果没有创建该文件,则操作失败),(3)当成功打开文件时停止重复步骤。

  C 程序员实际上不需要直接这样去做;只需要调用库函数 mkstemp(3),就可以打开文件。mkstemp(3) 的一些实现并没有将 umask(2) 设置为一个受限的值,所以聪明的做法是先调用 umask(2) 来强制将文件设置为受限的值。有一个小麻烦,mkstemp(3) 不直接支持 TMP 或 TMPDIR 环境变量,所以,如果这对您来说是重要的,那么您必须做更多的工作。

  当为了安全地打开临时文件而在共享(临时)目录中创建文件系统对象时,GNOME 编程向导推荐您使用下面的 C 代码:

清单 3. 推荐使用的创建临时文件的 C 代码

char *filename;
int fd;
do {
  filename = tempnam (NULL, "foo");
  fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
  free (filename);
} while (fd == -1);

  注意,尽管使用了不安全的 tempnam(3) 函数,但是它被包装在循环中,并使用了 O_CREAT 和 O_EXCL 选项,从而抵消了它的安全弱点,所以这样做是可以的。一个附带好处是,tempnam(3) 通常使用 TMPDIR,这使得用户可以重定向其临时文件,如果需要的话。注意,您需要 free() 文件名称。完成使用之后,您应该 close() 并 unlink() 文件。这种方法有一个小的缺点,由于可能无法安全使用 tempnam,所以各种编译器和安全扫描器可能都会向您发出使用不合逻辑的警告。使用 mkstemp(3) 就不存在这个问题。

  整个这个打开文件的方法展示出了标准的 C IO 库的一个奇特之处:没有标准的方法来指定 O_EXCL 选项使用 fopen(),所以您不能以“普通的” C 方式来打开文件,并安全地创建临时文件。如果您想使用标准 C IO 库,然后使用 open(),那么您可以使用指定“w+b” 模式的 fdopen(),将文件描述符转换为一个 FILE *。

  Perl 程序员应该使用 File::Temp,它尝试提供一个安全创建临时文件的跨平台方法。不过,首先要仔细阅读如何正确使用它的文档;它同样有不安全的函数接口。我建议您显式地将 safe_level 设置为 HIGH;这样就会调用附加的安全检查。对大部分编程库来说都是如此;大部分库都既有安全接口,又有不安全接口,所以您需要查阅文档,并确保选择了安全的版本。

  注意,在旧版的 NFS(版本 1 或者版本 2)的目录上使用 O_EXCL 是没有用的,因为 NFS 的这些旧版本没有正确地支持 O_EXCL。如果您自己使用了 O_EXCL,而且共享目录是使用这些旧版 NFS 实现的,那么您的程序将是不安全的。实际上,关于旧版 NFS 中 link(2) 和 stat(2) 的使用有一个复杂的解决方法;如果您的程序一定要在这样的环境中工作,那么您可以在 Linux 的 open(2)手册页或者其他地方阅读关于该方法的内容。在这里我不准备对它进行讨论,因为即使您的程序可以与旧版的 NFS 一起使用,您所使用的很多其他程序也不会使用该解决方法。无论如何您都不可能获得使用 NFS 版本 1 或者版本 2 的临时目录的安全系统,因为其他程序没有使用该解决方法,所以,如果使用远程挂载的临时目录,更明智的做法是要求使用 NFS 版本 3 或更高版本。

  您可以尝试使用 mkdtemp(3),但这通常不是一个好主意,因为临时文件清除器(temp cleaners)可能会决定清除它们。

  如果您正在编写 shell 脚本,那么可以使用管道,或者在用户的主目录存入临时文件。根本不要使用 /tmp 或 /var/tmp 目录;普通的 shell 通常无法支持文件描述符,所以临时文件清除器(tmpfile cleaners)最终将使它们失败。如果没有临时文件清除器,而且您只是必须在 /tmp 中创建临时文件,那么至少要使用 mktemp(1) 来防止更明显的攻击,因为 mktemp(1)(不是 mktemp(3))将使用 O_EXCL 来防止典型的竞争条件攻击。您可能做的最糟糕事情通常也是最令人不安的事:假定“$$”没有被攻击者猜出来,并且只是将信息重定向到这类文件;那么在创建时就不会按要求使用 O_EXCL 模式。攻击者可以简单地预创建类似的文件,或者反复创建和删除它们,最终接管程序。这样,类似的 shell 脚本几乎肯定有一个严重的缺陷:

清单 4. 有缺陷的 shell 脚本

  echo "This is a test" > /tmp/test$$ # DON'T DO THIS.

  不要再次
相关阅读