在Python中避免文件权限检查的竞争条件

1 投票
5 回答
1913 浏览
提问于 2025-04-15 19:14

一个应用程序想要解析并“执行”一个文件,并且出于安全原因,它希望确认这个文件是可以执行的。

稍微想一下,你就会发现这段初始代码存在一个竞争条件,这让安全措施变得无效:

import os

class ExecutionError (Exception):
    pass

def execute_file(filepath):
    """Execute serialized command inside @filepath

    The file must be executable (comparable to a shell script)
    >>> execute_file(__file__)  # doctest: +ELLIPSIS
    Traceback (most recent call last):
        ...
    ExecutionError: ... (not executable)
    """
    if not os.path.exists(filepath):
        raise IOError('"%s" does not exist' % (filepath, ))
    if not os.access(filepath, os.X_OK):
        raise ExecutionError('No permission to run "%s" (not executable)' %
                filepath)

    data = open(filepath).read()

    print '"Dummy execute"'
    print data

这个竞争条件存在于

os.access(filepath, os.X_OK)

data = open(filepath).read()

之间,因为在这两个系统调用之间,文件有可能被覆盖成一个内容不同且不可执行的文件。

我第一个想到的解决方案是改变关键调用的顺序(并跳过现在多余的存在性检查):

fobj = open(filepath, "rb")
if not os.access(filepath, os.X_OK):
    raise ExecutionError('No permission to run "%s" (not executable)' %
            filepath)

data = fobj.read()

这样能解决竞争条件吗?我该如何正确解决这个问题呢?

安全方案的基本思路(我认为)

这个文件能够在它的环境中执行任意命令,所以它可以和一个 shell 脚本相提并论。

在免费的桌面环境中,有一个关于 .desktop 文件的安全漏洞,这些文件定义了应用程序:文件可以指定任何可执行文件和参数,并且可以选择自己的图标和名称。因此,一个随机下载的文件可以伪装成任何名称或图标,做任何事情。这是非常糟糕的。

这个问题的解决办法是要求 .desktop 文件必须设置可执行位,否则它们不会显示名称和图标,系统会在启动程序之前询问用户是否要继续。

这和 Mac OS X 的设计非常好地对比:“这个程序是从网上下载的,你确定要打开吗?”

所以结合这个比喻,以及你必须对下载的 shell 脚本使用 chmod +x 的事实,我想到了上面问题中的设计。

结束语

也许总结一下,我们应该保持简单:如果文件必须是可执行的,就让它可执行,并让内核在用户调用时执行它。把任务委托给它应该去的地方。

5 个回答

0

你应该更改文件的拥有者,这样攻击者就无法访问它。可以用命令“chown root:root 文件名”来实现。然后再用“chmod 700 文件名”这个命令,这样其他账户就不能读取、写入或执行这个文件了。这样就完全避免了TOCTOU(时间竞争漏洞)的问题,这也是人们用来防止攻击者在你的系统上修改文件的一种方法。

2

你无法完全解决这个竞争条件问题——比如在你先打开文件,然后检查权限的情况下,文件的权限有可能在你打开文件后、检查权限前就被改变了。

如果你能把文件安全地移动到一个坏人无法到达的目录里,那么你就可以放心,在你处理这个文件的时候,它的内容不会被悄悄改动。如果坏人能到达任何地方,或者你无法把文件移动到他们到不了的地方,那就没有什么防御措施了。

顺便说一下,我不太明白即使这个方案能成功实施,它到底能增加什么安全性——如果坏人能在文件里放入恶意内容,他们难道就不能把文件的权限改成可执行吗?

3

文件的可执行性是和你打开的文件绑定在一起的,多个文件可以指向同一个包含你想读取数据的inode。换句话说,同样的数据可以从文件系统中其他非可执行的文件读取。此外,即使你已经打开了这个文件,也无法阻止这个文件的可执行性发生变化,它甚至可能被删除。

我认为你能做的“最大努力”就是使用 os.fstat 对打开的文件进行检查,查看它的保护模式和修改时间,比较打开前后的状态,但即使这样,也只能减少在你读取文件时未发现变化的可能性。

再想想,如果你是这个文件中数据的原始创建者,你可以考虑写一个从未链接到文件系统的inode,这是一种在文件间共享内存的常见技术。或者,如果这些数据最终必须公开给其他用户,你可以使用文件锁定,然后逐步扩展保护权限给需要的用户。

最终,你必须确保恶意用户根本没有写入这个文件的权限。

撰写回答