在使用access() 进行文件存在性检查时,是否存在TOCTOU漏洞?
阅读关于时间-检查到时间-使用(TOCTOU)漏洞的资料,我在示例中多次看到 access() 函数被引用。典型的情况是它在打开文件之前被调用以检查该文件是否具有适当的权限。如下片段所示。
int write_file(const char* path)
{
if (access(path, W_OK))
return BAD_FILE;
...
FILE* file = fopen(path, "w");
...
}
这是一种TOCTOU漏洞,因为在对 access() 的调用与对 fopen() 的调用之间的这段时间内,攻击者可能会修改位于 path 的文件。关于TOCTOU的维基百科条目给出了一个示例:攻击者用一个指向 /etc/passwd 的符号链接替换该文件,这将导致程序(若具备足够的权限运行)以写权限打开 /etc/passwd,尽管攻击者本人对该文件并没有写访问权限。
据我所知,这类漏洞的原因主要来自于 access() 对文件访问权限的行为。在这段代码中,access() 调用的目的是在打开文件前确保实际UID对该文件具有写权限(因为 fopen() 只检查有效UID)。进程的实际UID可能对 fileA 没有写访问权限,因此对 fileA 的 access() 调用将失败。但如果在易受攻击的时间段内,定位的文件 path 被改成了 fileA,则实际UID的检查在实质上被绕过。
我也看到这种推理被用来劝阻使用 access() 函数。例如,POSIX.1-2024 说:
使用 [access()] 被劝阻,因为在对返回信息采取行动时,它已是过时的。
以及Linux手册页(版本6.16)指出:
使用 [access(2)] 来检查用户是否被授权在实际使用open(2) 打开文件之前,就已经造成了安全漏洞……因此应避免使用该系统调用。
然而,access() 函数也可以仅用于检查给定文件是否存在(使用 F_OK 模式)。通过如下修改前面的片段即可看到这一点:
int write_file(const char* path)
{
if (access(path, F_OK))
return NO_FILE;
...
FILE* file = fopen(path, "w");
...
}
现在如果说的文件不存在,程序将拒绝对位于 path 的文件调用 fopen();它检查的是文件是否存在而非文件访问。显然,在这个例子中,access() 的检查是多余的——如果文件不存在,fopen() 将失败。但忽略这一点,我想知道:这种对 access() 的使用是否也会引入TOCTOU安全漏洞?
我能想到的唯一此类攻击是在 access() 与 fopen() 调用之间,攻击者删除了由 path 定位的文件。但只要程序能够处理 fopen() 失败,我看不出这会如何成为漏洞,或引发漏洞。如果这确实不是一个漏洞,是否在对该函数的使用存在警告的情况下,仍然合理地调用 access() 来检查文件存在性呢?
为说明我的意思,下面给出一个示例用例。
int pick_file(const char** paths, int count)
{
for (int i = 0; i < count; i++) {
...
if (!access(paths[i], F_OK))
return i;
}
return -1;
}
pick_file() 函数返回(可能还有其他检查)值为一个路径的第一个 paths 的索引,该路径对应一个存在的文件。这个函数可用于在程序中多次选择合适的文件进行访问(打开、读取或写入)。在这种情形下,access() 用于在打开文件之前检查其是否存在。
那么,像本例这样用 access() 进行文件存在性检查,是否可能引发TOCTOU安全漏洞呢?
解决方案
那么,像本例一样用
access()对文件存在性进行检查,是否也可能引发TOCTOU漏洞?
是的,它确实会。
你在调用 access() 时知道该文件曾经存在并返回了它的索引,但在检查之后该文件可能已被删除。如果你利用该索引在某种程度上授予权限,或者你假设在返回的索引之前的 paths 中不存在任何文件并据此让程序作出决策,可能会做出错误的决定。错误的决定可能导致安全漏洞。
该函数本质上只是 access() 的一个扩展,应该具有与你在 access() 处看到的相同提示:
使用 pick_file() 被劝阻,因为在对返回信息采取行动时,它已经是过时的。