Как защитить себя от бомбы 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