如何保护自己免受 gzip 或 bzip2 炸弹的伤害?
Posted
技术标签:
【中文标题】如何保护自己免受 gzip 或 bzip2 炸弹的伤害?【英文标题】:How to protect myself from a gzip or bzip2 bomb? 【发布时间】:2012-11-17 08:16:48 【问题描述】:这与问题about zip bombs 有关,但要考虑 gzip 或 bzip2 压缩,例如接受.tar.gz
文件的网络服务。
Python 提供了一个方便的tarfile module,使用起来很方便,但似乎没有提供针对 zipbombs 的保护。
在使用 tarfile 模块的 python 代码中,检测 zip 炸弹最优雅的方法是什么,最好不要从 tarfile 模块复制太多逻辑(例如透明解压缩支持)?
而且,只是为了让它不那么简单:不涉及真正的文件;输入是一个类似文件的对象(由网络框架提供,代表用户上传的文件)。
【问题讨论】:
你不能使用 TarInfo.size 吗? @fatfredyy 你可以在解压 tar 之前点击 gz 炸弹。 你担心炸弹的什么影响?仅使用内存?提取时还有磁盘空间使用情况(根据引用的问题)? 叹息。似乎有些人认为这是一个系统管理员问题(可以通过快速阅读来解决)。所以我稍微澄清了这个问题:这实际上是关于编写使 Web 应用程序 gzip-bomp-safe 的代码。 相关:gzip, bz2, lzma: add option to limit output size 【参考方案1】:我还需要处理上传的 zip 文件中的 zip 炸弹。
我通过创建一个固定大小的 tmpfs 并解压缩到它来做到这一点。如果提取的数据太大,则 tmpfs 将耗尽空间并报错。
这是创建 200M tmpfs 以解压缩到的 linux 命令。
sudo mkdir -p /mnt/ziptmpfs
echo 'tmpfs /mnt/ziptmpfs tmpfs rw,nodev,nosuid,size=200M 0 0' | sudo tee -a /etc/fstab
【讨论】:
【参考方案2】:这将确定 gzip 流的未压缩大小,同时使用有限的内存:
#!/usr/bin/python
import sys
import zlib
f = open(sys.argv[1], "rb")
z = zlib.decompressobj(15+16)
total = 0
while True:
buf = z.unconsumed_tail
if buf == "":
buf = f.read(1024)
if buf == "":
break
got = z.decompress(buf, 4096)
if got == "":
break
total += len(got)
print total
if z.unused_data != "" or f.read(1024) != "":
print "warning: more input after end of gzip stream"
它会返回稍微高估的 tar 文件中的所有文件在提取时所需的空间。长度包括那些文件,以及 tar 目录信息。
gzip.py 代码不控制解压缩的数据量,除了输入数据的大小。在 gzip.py 中,它一次读取 1024 个压缩字节。因此,如果您对未压缩数据的内存使用量最多约为 1056768 字节(1032 * 1024,其中 1032:1 是 deflate 的最大压缩比)没有问题,则可以使用 gzip.py。这里的解决方案使用zlib.decompress
和第二个参数,它限制了未压缩数据的数量。 gzip.py 没有。
这将通过解码 tar 格式准确地确定提取的 tar 条目的总大小:
#!/usr/bin/python
import sys
import zlib
def decompn(f, z, n):
"""Return n uncompressed bytes, or fewer if at the end of the compressed
stream. This only decompresses as much as necessary, in order to
avoid excessive memory usage for highly compressed input.
"""
blk = ""
while len(blk) < n:
buf = z.unconsumed_tail
if buf == "":
buf = f.read(1024)
got = z.decompress(buf, n - len(blk))
blk += got
if got == "":
break
return blk
f = open(sys.argv[1], "rb")
z = zlib.decompressobj(15+16)
total = 0
left = 0
while True:
blk = decompn(f, z, 512)
if len(blk) < 512:
break
if left == 0:
if blk == "\0"*512:
continue
if blk[156] in ["1", "2", "3", "4", "5", "6"]:
continue
if blk[124] == 0x80:
size = 0
for i in range(125, 136):
size <<= 8
size += blk[i]
else:
size = int(blk[124:136].split()[0].split("\0")[0], 8)
if blk[156] not in ["x", "g", "X", "L", "K"]:
total += size
left = (size + 511) // 512
else:
left -= 1
print total
if blk != "":
print "warning: partial final block"
if left != 0:
print "warning: tar file ended in the middle of an entry"
if z.unused_data != "" or f.read(1024) != "":
print "warning: more input after end of gzip stream"
您可以使用它的变体来扫描 tar 文件中的炸弹。这样做的好处是可以在您不必解压缩该数据之前在标头信息中找到较大的尺寸。
对于 .tar.bz2 档案,Python bz2 库(至少从 3.3 开始)对于消耗过多内存的 bz2 炸弹不可避免地不安全。 bz2.decompress
函数不像 zlib.decompress
那样提供第二个参数。更糟糕的是,由于运行长度编码,bz2 格式的最大压缩率比 zlib 高得多。 bzip2 将 1 GB 的零压缩为 722 字节。因此,即使没有第二个参数,您也无法像 zlib.decompress
那样通过测量输入来测量 bz2.decompress
的输出。对解压后的输出大小没有限制是 Python 接口的一个根本缺陷。
我查看了 3.3 中的 _bz2module.c 以查看是否有未记录的方式来使用它来避免此问题。没有其他办法了。那里的decompress
函数只是不断增长结果缓冲区,直到它可以解压缩所有提供的输入。 _bz2module.c 需要修复。
【讨论】:
你确定这行得通吗? tar 如何在不解压缩 gzip 包装器的情况下知道大小?请注意,我担心的是 gzip 炸弹,而不是 tar 炸弹! 我测试了它,它不起作用:将一个 10GB 的零文件打包到一个.tar.gz
文件中会产生一个 10MB 的大文件。在设置了ulimit -v 200000
的文件上运行您的代码失败,因此它使用的输入远远超过 10MB,因此容易受到 zipbombs 的影响。
好的,这可能会起作用,因为z.decompress
是安全的,但是对于 bzip2 不起作用(除非我遗漏了一些东西),并且由于 bzip 库 API 的缺点而不能轻易采用。此外,它似乎比我解决方案中的代码更复杂、更容易出错。
gzip.py 代码不控制解压缩的数据量,除了输入数据的大小。在 gzip.py 中,它一次读取 1024 个压缩字节。因此,如果您对未压缩数据(1032 * 1024,其中 1032:1 是 deflate 的最大压缩比)最多使用大约 1056768 字节的内存没问题,那么您的方法将起作用。我的解决方案使用带有第二个参数的 zlib.decompress,它限制了未压缩数据的数量。 gzip.py 没有。
您是正确的,没有使用 Python bz2 库的内存安全方式。 bz2.decompress 函数不像 zlib.decompress 那样提供第二个参数。更糟糕的是,由于运行长度编码,bz2 格式的最大压缩率比 zlib 高得多。 bzip2 将 1 GB 的零压缩为 722 字节。因此,即使没有第二个参数,您也无法像 zlib.decompress 那样通过测量输入来测量 bz2.decompress 的输出。对解压后的输出大小没有限制是 Python 接口的一个根本缺陷。【参考方案3】:
您可以使用resource
module 来限制您的进程及其子进程可用的资源。
如果您需要在内存中解压缩,那么您可以设置resource.RLIMIT_AS
(或RLIMIT_DATA
、RLIMIT_STACK
),例如,使用上下文管理器将其自动恢复到以前的值:
import contextlib
import resource
@contextlib.contextmanager
def limit(limit, type=resource.RLIMIT_AS):
soft_limit, hard_limit = resource.getrlimit(type)
resource.setrlimit(type, (limit, hard_limit)) # set soft limit
try:
yield
finally:
resource.setrlimit(type, (soft_limit, hard_limit)) # restore
with limit(1 << 30): # 1GB
# do the thing that might try to consume all memory
如果达到限制; MemoryError
被提出。
【讨论】:
无论存档大小或未压缩数据量如何,正确实施的 tar.gz 提取器都没有理由占用超过 40K 的内存。提取时占用的磁盘空间量是另一回事,但这无济于事。 @MarkAdler:OP只对所有数据都在内存中的情况感兴趣:“不涉及真实文件”,“我有数据记忆” 发帖者链接的问题将 zip 炸弹问题描述为 “打开时它们会填满服务器的磁盘。”。所以不清楚。 在任何情况下,您都不需要任何可观的内存来检查或提取 .tar.gz 文件,无论其大小。 在处理不受信任的数据时,这是一种可能性,也可能是一个很好的一般预防措施。缺点是很难提前知道何时处理失败。如果该代码不太适合在异常后回滚,则最好提前检查它是否安全。【参考方案4】:我想答案是:没有简单的现成解决方案。这是我现在使用的:
class SafeUncompressor(object):
"""Small proxy class that enables external file object
support for uncompressed, bzip2 and gzip files. Works transparently, and
supports a maximum size to avoid zipbombs.
"""
blocksize = 16 * 1024
class FileTooLarge(Exception):
pass
def __init__(self, fileobj, maxsize=10*1024*1024):
self.fileobj = fileobj
self.name = getattr(self.fileobj, "name", None)
self.maxsize = maxsize
self.init()
def init(self):
import bz2
import gzip
self.pos = 0
self.fileobj.seek(0)
self.buf = ""
self.format = "plain"
magic = self.fileobj.read(2)
if magic == '\037\213':
self.format = "gzip"
self.gzipobj = gzip.GzipFile(fileobj = self.fileobj, mode = 'r')
elif magic == 'BZ':
raise IOError, "bzip2 support in SafeUncompressor disabled, as self.bz2obj.decompress is not safe"
self.format = "bz2"
self.bz2obj = bz2.BZ2Decompressor()
self.fileobj.seek(0)
def read(self, size):
b = [self.buf]
x = len(self.buf)
while x < size:
if self.format == 'gzip':
data = self.gzipobj.read(self.blocksize)
if not data:
break
elif self.format == 'bz2':
raw = self.fileobj.read(self.blocksize)
if not raw:
break
# this can already bomb here, to some extend.
# so disable bzip support until resolved.
# Also monitor http://***.com/questions/13622706/how-to-protect-myself-from-a-gzip-or-bzip2-bomb for ideas
data = self.bz2obj.decompress(raw)
else:
data = self.fileobj.read(self.blocksize)
if not data:
break
b.append(data)
x += len(data)
if self.pos + x > self.maxsize:
self.buf = ""
self.pos = 0
raise SafeUncompressor.FileTooLarge, "Compressed file too large"
self.buf = "".join(b)
buf = self.buf[:size]
self.buf = self.buf[size:]
self.pos += len(buf)
return buf
def seek(self, pos, whence=0):
if whence != 0:
raise IOError, "SafeUncompressor only supports whence=0"
if pos < self.pos:
self.init()
self.read(pos - self.pos)
def tell(self):
return self.pos
它不适用于 bzip2,因此部分代码被禁用。原因是bz2.BZ2Decompressor.decompress
已经可以产生大量不需要的数据。
【讨论】:
【参考方案5】:如果你是为linux开发的,你可以在单独的进程中运行解压,并使用ulimit来限制内存使用。
import subprocess
subprocess.Popen("ulimit -v %d; ./decompression_script.py %s" % (LIMIT, FILE))
请记住,decompression_script.py 应该在写入磁盘之前解压缩内存中的整个文件。
【讨论】:
这可能行得通,但这不符合优雅的条件。此外,我需要将数据通过管道传输到脚本中,这使得与 ulimit 调用结合起来有点困难。 你可以只创建一个文件,不需要管道,在这种情况下可以编译。 但是我在内存中有数据;为什么我必须在这里处理文件?以上是关于如何保护自己免受 gzip 或 bzip2 炸弹的伤害?的主要内容,如果未能解决你的问题,请参考以下文章