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.