python security gzip bzip2

python - ¿Cómo protegerme de una bomba gzip o bzip2?



security (4)

Esto está relacionado con la pregunta sobre las bombas zip , pero teniendo en cuenta la compresión gzip o bzip2, por ejemplo, un servicio web que acepta archivos .tar.gz .

Python proporciona un módulo tarfile práctico que es conveniente de usar, pero que no parece proporcionar protección contra zipbombs.

En el código python que usa el módulo tarfile, ¿cuál sería la forma más elegante de detectar bombas zip, preferiblemente sin duplicar demasiada lógica (por ejemplo, el soporte de descompresión transparente) del módulo tarfile?

Y, simplemente para hacerlo un poco menos simple: no hay archivos reales involucrados; la entrada es un objeto similar a un archivo (proporcionado por el marco web, que representa el archivo que un usuario cargó).


Esto determinará el tamaño sin comprimir del flujo de gzip, mientras se usa una memoria limitada:

#!/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"

Devolverá una pequeña sobreestimación del espacio requerido para todos los archivos en el archivo tar cuando se extraiga. La longitud incluye esos archivos, así como la información del directorio tar.

El código gzip.py no controla la cantidad de datos descomprimidos, excepto en virtud del tamaño de los datos de entrada. En gzip.py, lee 1024 bytes comprimidos a la vez. Por lo tanto, puede usar gzip.py si está de acuerdo con hasta 1056768 bytes de uso de memoria para los datos no comprimidos (1032 * 1024, donde 1032: 1 es la relación de compresión máxima del desinflado). La solución aquí utiliza zlib.decompress con el segundo argumento, que limita la cantidad de datos sin comprimir. gzip.py no lo hace.

Esto determinará con precisión el tamaño total de las entradas de tar extraídas al decodificar el formato de 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"

Podría usar una variante de esto para escanear el archivo tar en busca de bombas. Esto tiene la ventaja de encontrar un gran tamaño en la información del encabezado antes de que incluso tenga que descomprimir esos datos.

En cuanto a los archivos .tar.bz2, la biblioteca bz2 de Python (al menos a partir de 3.3) es inevitablemente insegura para las bombas bz2 que consumen demasiada memoria. La función bz2.decompress no ofrece un segundo argumento como zlib.decompress . Esto se hace aún peor por el hecho de que el formato bz2 tiene una relación de compresión máxima mucho, mucho más alta que zlib debido a la codificación de longitud de ejecución. bzip2 comprime 1 GB de ceros a 722 bytes. Por lo tanto, no puede medir la salida de bz2.decompress midiendo la entrada como se puede hacer con zlib.decompress incluso sin el segundo argumento. La falta de un límite en el tamaño de salida descomprimido es un defecto fundamental en la interfaz de Python.

Busqué en _bz2module.c en 3.3 para ver si hay una forma no documentada de usarlo para evitar este problema. No hay manera de evitarlo. La función de decompress allí sigue aumentando el búfer de resultados hasta que pueda descomprimir toda la entrada proporcionada. _bz2module.c necesita ser arreglado.


Podría usar el módulo de resource para limitar los recursos disponibles para su proceso y sus hijos.

Si necesita descomprimir en la memoria, entonces puede configurar resource.RLIMIT_AS (o RLIMIT_DATA , RLIMIT_STACK ), por ejemplo, usando un administrador de contexto para restaurarla automáticamente a un valor anterior:

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

Si se alcanza el límite; MemoryError es elevado.


Si desarrolla para Linux, puede ejecutar la descompresión en un proceso separado y usar ulimit para limitar el uso de la memoria.

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

Tenga en cuenta que decompression_script.py debe descomprimir todo el archivo en la memoria, antes de escribir en el disco.


Supongo que la respuesta es: no hay una solución fácil, ya hecha. Esto es lo que uso ahora:

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

No funciona bien para bzip2, por lo que parte del código está deshabilitado. La razón es que bz2.BZ2Decompressor.decompress ya puede producir una gran cantidad de datos no deseados.