这是有关这个问题有关的zip炸弹 ,但有gzip的或bzip2的心态压缩,如Web服务接受.tar.gz
文件。
Python提供了一个方便的tar文件模块,这是方便使用,但似乎并没有提供保护,防止zipbombs。
在使用tar文件模块Python代码,这将是检测压缩炸弹,最好不从tar文件复制模块太多逻辑(例如透明的解压支持)最优雅的方式?
而且,这只是为了少一点简单:没有真正的文件都参与; 输入是一个类文件对象(由web框架提供,表示文件上传的用户)。
这是有关这个问题有关的zip炸弹 ,但有gzip的或bzip2的心态压缩,如Web服务接受.tar.gz
文件。
Python提供了一个方便的tar文件模块,这是方便使用,但似乎并没有提供保护,防止zipbombs。
在使用tar文件模块Python代码,这将是检测压缩炸弹,最好不从tar文件复制模块太多逻辑(例如透明的解压支持)最优雅的方式?
而且,这只是为了少一点简单:没有真正的文件都参与; 输入是一个类文件对象(由web框架提供,表示文件上传的用户)。
你可以使用resource
模块可用资源限制对过程和它的孩子。
如果你需要在内存中解压缩,那么你可以设置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
提高。
这将决定的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文件中的文件所需的空间略有高估。 长度包括这些文件,以及焦油目录信息。
所述gzip.py代码不控制解压缩数据的量,除了借助于所述输入数据的大小。 在gzip.py,它每次读取1024个压缩字节。 所以,如果你确定与未压缩数据(:1是放气的最大压缩比1032 * 1024,1032)高达约1056768个字节的内存使用情况,您可以使用gzip.py。 这里该解决方案使用zlib.decompress
与第二个参数,这限制了未压缩的数据量。 gzip.py没有。
这将准确地由焦油格式进行解码确定所提取的条目焦油的总大小:
#!/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个的字节。 所以,你可以不计的输出bz2.decompress
通过计量输入作为是可以做到的zlib.decompress
即使没有第二个参数。 缺乏对解压后的输出大小的限制是在Python接口一个根本的缺陷。
我看着在3.3 _bz2module.c,看是否有使用它来避免这个问题的无证方式。 没有其他办法了。 在decompress
在那里功能只是不断增长的结果缓冲区,直到它可以解压缩所提供的所有投入。 _bz2module.c需要固定。
如果开发Linux,你可以在单独的进程中运行解压缩和使用的ulimit限制内存的使用。
import subprocess
subprocess.Popen("ulimit -v %d; ./decompression_script.py %s" % (LIMIT, FILE))
请记住,decompression_script.py应解压缩在内存中的整个文件,写入磁盘之前。
我想答案是:有没有简单的,现成的解决方案。 以下是我现在使用:
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://stackoverflow.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
已经可以产生不必要的大块数据。
我还需要处理压缩炸弹上传zipfiles。
我通过创建一个固定大小的tmpfs,并解压到做到这一点。 如果提取的数据过大,则TMPFS会用完的空间,并给出错误。
这里是Linux命令创建一个200M的tmpfs解压到。
sudo mkdir -p /mnt/ziptmpfs
echo 'tmpfs /mnt/ziptmpfs tmpfs rw,nodev,nosuid,size=200M 0 0' | sudo tee -a /etc/fstab