Как защитить себя от бомбы gzip или bzip2?
Это связано с вопросом о zip-бомбах, но с учетом сжатия gzip или bzip2, например. веб-служба, принимающая файлы .tar.gz
.
Python предоставляет удобный > tarfile module, который удобен в использовании, но, похоже, не обеспечивает защиту от zipbombs.
В коде python, использующем модуль tarfile, какой из самых элегантных способов можно было бы обнаружить в молнии, желательно, не дублируя слишком много логики (например, прозрачную поддержку декомпрессии) из модуля tarfile?
И просто сделать это немного проще: никаких реальных файлов не задействовано; вход является файлоподобным объектом (предоставляемым веб-каркасом, представляющим файл, загруженный пользователем).
Ответы
Ответ 1
Я думаю, что ответ таков: нет легкого, готового решения. Вот что я сейчас использую:
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
уже может создать нежелательный большой фрагмент данных.
Ответ 2
Вы можете использовать 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
.
Ответ 3
Это определит несжатый размер потока 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 сжатых байта за раз. Таким образом, вы можете использовать gzip.py, если вы используете до 1056768 байт использования памяти для несжатых данных (1032 * 1024, где 1032: 1 - максимальная степень сжатия дефлята). В решении здесь используется 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 ГБ нулей до 722 байта. Таким образом, вы не можете измерить вывод bz2.decompress
путем измерения ввода, как это может быть сделано с помощью zlib.decompress
, даже без второго аргумента. Отсутствие ограничения на размер распакованного выходного файла является фундаментальным недостатком интерфейса Python.
Я просмотрел файл _bz2module.c в 3.3, чтобы узнать, существует ли недокументированный способ его использования, чтобы избежать этой проблемы. Об этом нет. Функция decompress
там только продолжает увеличивать буфер результатов, пока не сможет распаковать весь предоставленный вход. _bz2module.c должен быть исправлен.
Ответ 4
Если вы разрабатываете для linux, вы можете запустить декомпрессию в отдельном процессе и использовать ulimit для ограничения использования памяти.
import subprocess
subprocess.Popen("ulimit -v %d; ./decompression_script.py %s" % (LIMIT, FILE))
Имейте в виду, что файл decpression_script.py должен распаковать весь файл в памяти перед записью на диск.
Ответ 5
Мне также нужно обрабатывать бомбы в загруженных файлах.
Я делаю это, создавая фиксированный размер 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