在Python中避免文件权限检查的竞争条件
一个应用程序想要解析并“执行”一个文件,并且出于安全原因,它希望确认这个文件是可以执行的。
稍微想一下,你就会发现这段初始代码存在一个竞争条件,这让安全措施变得无效:
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 个回答
你应该更改文件的拥有者,这样攻击者就无法访问它。可以用命令“chown root:root 文件名”来实现。然后再用“chmod 700 文件名”这个命令,这样其他账户就不能读取、写入或执行这个文件了。这样就完全避免了TOCTOU(时间竞争漏洞)的问题,这也是人们用来防止攻击者在你的系统上修改文件的一种方法。
你无法完全解决这个竞争条件问题——比如在你先打开文件,然后检查权限的情况下,文件的权限有可能在你打开文件后、检查权限前就被改变了。
如果你能把文件安全地移动到一个坏人无法到达的目录里,那么你就可以放心,在你处理这个文件的时候,它的内容不会被悄悄改动。如果坏人能到达任何地方,或者你无法把文件移动到他们到不了的地方,那就没有什么防御措施了。
顺便说一下,我不太明白即使这个方案能成功实施,它到底能增加什么安全性——如果坏人能在文件里放入恶意内容,他们难道就不能把文件的权限改成可执行吗?
文件的可执行性是和你打开的文件绑定在一起的,多个文件可以指向同一个包含你想读取数据的inode。换句话说,同样的数据可以从文件系统中其他非可执行的文件读取。此外,即使你已经打开了这个文件,也无法阻止这个文件的可执行性发生变化,它甚至可能被删除。
我认为你能做的“最大努力”就是使用 os.fstat
对打开的文件进行检查,查看它的保护模式和修改时间,比较打开前后的状态,但即使这样,也只能减少在你读取文件时未发现变化的可能性。
再想想,如果你是这个文件中数据的原始创建者,你可以考虑写一个从未链接到文件系统的inode,这是一种在文件间共享内存的常见技术。或者,如果这些数据最终必须公开给其他用户,你可以使用文件锁定,然后逐步扩展保护权限给需要的用户。
最终,你必须确保恶意用户根本没有写入这个文件的权限。