Как защититься от бомбы gzip или bzip2?

Это связано с вопросом о zip-бомбах, но имея в виду сжатие gzip или bzip2, например веб-служба, принимающая .tar.gz файлы.

Python предоставляет удобный модуль tarfile, который удобен в использовании, но не кажется для защиты от бомб.

В коде Python, использующем модуль tarfile, что было бы наиболее элегантным способом обнаружения zip-бомб, желательно без дублирования излишней логики (например, поддержки прозрачной декомпрессии) из модуля tarfile?

И, чтобы упростить задачу: не задействованы никакие реальные файлы; входные данные представляют собой объект, подобный файлу (предоставляемый веб-фреймворком, представляющий файл, загруженный пользователем).


person Joachim Breitner    schedule 29.11.2012    source источник
comment
Разве вы не можете использовать TarInfo.size?   -  person damiankolasa    schedule 29.11.2012
comment
@fatfredyy, вы можете ударить gz-бомбу, прежде чем распаковать tar.   -  person Jakozaur    schedule 29.11.2012
comment
Какой эффект от бомбы вас беспокоит? Только использование памяти? Также использование дискового пространства при извлечении (согласно указанному вопросу)?   -  person Mark Adler    schedule 25.12.2012
comment
Хм, мой вопрос был отвергнут без объяснения причин, и я не понимаю, о чем идет речь: разве это не об очень четкой и конкретной задаче программирования?   -  person Joachim Breitner    schedule 10.12.2013
comment
Вздох. Похоже, что некоторые люди думают, что это вопрос системного администратора (и это возможно после беглого чтения). Итак, я немного прояснил этот вопрос: на самом деле речь идет о написании кода, который делает веб-приложение безопасным с помощью gzip-bomp.   -  person Joachim Breitner    schedule 11.12.2013
comment


Ответы (5)


Вы можете использовать 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 поднят.

person jfs    schedule 24.12.2012
comment
Нет никаких причин, по которым правильно реализованный экстрактор tar.gz занимал более 40 КБ памяти, независимо от размера архива или количества несжатых данных. Другое дело, сколько места на диске занято при извлечении, но это не поможет. - person Mark Adler; 25.12.2012
comment
@MarkAdler: OP интересует только тот случай, когда все данные находятся в памяти: Никакие реальные файлы не задействованы, У меня есть данные в памяти - person jfs; 25.12.2012
comment
Вопрос, связанный с плакатом, описывает проблему с zip-бомбой как При открытии они заполняют диск сервера.. Так что непонятно. - person Mark Adler; 25.12.2012
comment
В любом случае вам не потребуется значительная память для изучения или извлечения файла .tar.gz, независимо от его размера. - person Mark Adler; 25.12.2012
comment
Это возможность и, возможно, хорошая общая мера предосторожности при работе с ненадежными данными. Недостатком является то, что трудно заранее сказать, когда обработка не удалась. Если этот код не подходит для отката после исключения, предпочтительнее заранее проверить, безопасен ли он. - person Joachim Breitner; 25.12.2012
comment
Вы также можете добавить resource.RLIMIT_FSIZE, чтобы ограничить максимальный размер файла, который может создать процесс. Я не думаю, что это сработает для дочерних процессов. - person Tim Ludwinski; 16.12.2015
comment
@TimLudwinski: да, вы можете передать все, что подходит в вашем случае, поэтому я сделал type параметром, а не жестко его кодировал. В этом вопросе все данные находятся в памяти. - person jfs; 16.12.2015

Это определит несжатый размер потока 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 - максимальная степень сжатия deflate). В решении здесь используется 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 необходимо исправить.

person Mark Adler    schedule 23.12.2012
comment
Вы уверены, что это работает? Как tar узнает о размере, не распаковывая обертку gzip вокруг него? Обратите внимание, что меня беспокоит gzip-бомба, а не гудронная бомба! - person Joachim Breitner; 24.12.2012
comment
Я тестировал его, он не работает: для упаковки файла нулей размером 10 ГБ в файл .tar.gz получается большой файл размером 10 МБ. Выполнение кода в этом файле с установленным ulimit -v 200000 не удается, поэтому он использует гораздо больше, чем входные 10 МБ, и, следовательно, уязвим для zipbombs. - person Joachim Breitner; 24.12.2012
comment
Хорошо, это может сработать, поскольку z.decompress безопасно, но это не сработает для bzip2 (если я чего-то не упустил) и не может быть легко принят из-за недостатков API библиотеки bzip. Кроме того, он кажется более сложным и подверженным ошибкам, чем код в моем решении. - person Joachim Breitner; 25.12.2012
comment
Код gzip.py не контролирует объем распакованных данных, за исключением размера входных данных. В gzip.py он читает 1024 сжатых байта за раз. Таким образом, ваш метод будет работать, если вы в порядке с использованием памяти до 1056768 байт для несжатых данных (1032 * 1024, где 1032: 1 - максимальная степень сжатия deflate). В моем решении используется zlib.decompress со вторым аргументом, который ограничивает объем несжатых данных. gzip.py - нет. - person Mark Adler; 25.12.2012
comment
Вы правы в том, что не существует безопасного для памяти способа использования библиотеки Python bz2. Функция bz2.decompress не предлагает второй аргумент, как zlib.decompress. Это усугубляется тем фактом, что формат bz2 имеет гораздо более высокий максимальный коэффициент сжатия, чем zlib, из-за кодирования длин серий. bzip2 сжимает 1 ГБ нулей до 722 байтов. Таким образом, вы не можете измерить вывод bz2.decompress, измеряя ввод, как это можно сделать с помощью zlib.decompress, даже без второго аргумента. Отсутствие ограничения на размер распакованного вывода является фундаментальным недостатком интерфейса Python. - person Mark Adler; 25.12.2012
comment
Просто проверено, просмотрев _bz2module.c в 3.3. Нет никакого способа обойти это. Функция распаковки там просто продолжает увеличивать буфер результатов до тех пор, пока не сможет распаковать весь предоставленный ввод. _bz2module.c необходимо исправить. - person Mark Adler; 25.12.2012
comment
Ну, ваш первоначальный ответ не сработал - разве это не причина для -1? Однако ваше исследование библиотеки Python bz2 представляет собой ценную информацию; почему бы не переместить это из комментария в ответ? - person Joachim Breitner; 25.12.2012

Если вы разрабатываете для Linux, вы можете запустить распаковку в отдельном процессе и использовать ulimit для ограничения использования памяти.

import subprocess
subprocess.Popen("ulimit -v %d; ./decompression_script.py %s" % (LIMIT, FILE))

Имейте в виду, что decopression_script.py должен распаковать весь файл в памяти перед записью на диск.

person Jakozaur    schedule 29.11.2012
comment
Это могло бы сработать, но не считалось бы элегантным. Кроме того, мне нужно было передать данные в скрипт, что немного усложнило бы их объединение с вызовом ulimit. - person Joachim Breitner; 29.11.2012
comment
Вы можете просто создать файл без канала, который в этом случае можно было бы скомпилировать. - person Jakozaur; 30.11.2012
comment
Но у меня есть данные в памяти; зачем мне здесь работать с файлами? - person Joachim Breitner; 30.11.2012

Думаю, ответ таков: простого готового решения не существует. Вот что я использую сейчас:

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 уже может произвести нежелательный большой блок данных.

person Joachim Breitner    schedule 23.12.2012

Мне также нужно обрабатывать zip-бомбы в загруженных zip-файлах.

Я делаю это, создавая 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
person Duke Dougal    schedule 28.04.2019