decompress - Extraiga de forma segura zip o tar utilizando Python
python generate zip file (4)
Al contrario de la respuesta popular, descomprimir archivos de forma segura no se resuelve completamente a partir de Python 2.7.4. El método de extracción es aún peligroso y puede llevar a un recorrido transversal, ya sea directamente o mediante el descompresión de enlaces simbólicos. Aquí estaba mi solución final que debería evitar los dos ataques en todas las versiones de Python, incluso las versiones anteriores a Python 2.7.4 donde el método de extracción era vulnerable:
import zipfile, os
def safe_unzip(zip_file, extractpath=''.''):
with zipfile.ZipFile(zip_file, ''r'') as zf:
for member in zf.infolist():
abspath = os.path.abspath(os.path.join(extractpath, member.filename))
if abspath.startswith(os.path.abspath(extractpath)):
zf.extract(member, extractpath)
Editado: Choque fijo de nombre de variable. Gracias Juuso Ohtonen.
Estoy intentando extraer los archivos zip y tar enviados por el usuario a un directorio. La documentación para el método extractall de zipfile (de manera similar a extractall de extractall ) indica que es posible que las rutas sean absolutas o contengan ..
rutas que van fuera de la ruta de destino. En su lugar, podría usar extract
mí mismo, así:
some_path = ''/destination/path''
some_zip = ''/some/file.zip''
zipf = zipfile.ZipFile(some_zip, mode=''r'')
for subfile in zipf.namelist():
zipf.extract(subfile, some_path)
¿Es esto seguro? ¿Es posible que un archivo en el archivo some_path
fuera de some_path
en este caso? Si es así, ¿de qué manera puedo garantizar que los archivos nunca terminen fuera del directorio de destino?
Copie el archivo zip en un directorio vacío. Luego use os.chroot
para hacer que ese directorio sea el directorio raíz. Luego descomprime allí.
Alternativamente, puede llamar a unzip
sí mismo con la bandera -j
, que ignora los directorios:
import subprocess
filename = ''/some/file.zip''
rv = subprocess.call([''unzip'', ''-j'', filename])
Use ZipFile.infolist()
/ TarFile.next()
/ TarFile.getmembers()
para obtener la información sobre cada entrada en el archivo, normalice la ruta, abra el archivo usted mismo, use ZipFile.open()
/ TarFile.extractfile()
para obtener un archivo similar a la entrada y copie los datos de la entrada usted mismo.
Nota: A partir de python 2.7.4, esto no es un problema para los archivos ZIP. Detalles al final de la respuesta. Esta respuesta se centra en los archivos de alquitrán.
Para averiguar a dónde apunta realmente un camino, use os.path.abspath()
(pero tenga en cuenta la advertencia sobre los enlaces simbólicos como componentes del camino). Si normaliza una ruta desde su archivo zip con abspath
y no contiene el directorio actual como prefijo, está apuntando hacia afuera.
Pero también necesita verificar el valor de cualquier enlace simbólico extraído de su archivo (tanto los archivos tar como los archivos zip de unix pueden almacenar enlaces simbólicos). Esto es importante si le preocupa un "usuario malicioso" proverbial que intencionalmente evitaría su seguridad, en lugar de una aplicación que simplemente se instala en las bibliotecas del sistema.
Esa es la advertencia mencionada anteriormente: abspath
se confundirá si su sandbox ya contiene un enlace simbólico que apunta a un directorio. Incluso un enlace simbólico que apunta dentro del recinto de seguridad puede ser peligroso: el enlace simbólico sandbox/subdir/foo -> ..
apunta a sandbox
, por lo que el camino sandbox/subdir/foo/../.bashrc
debe ser rechazado. La forma más sencilla de hacerlo es esperar hasta que se hayan extraído los archivos anteriores y usar os.path.realpath()
. Afortunadamente, extractall()
acepta un generador, por lo que es fácil de hacer.
Como solicitas código, aquí hay un poco que explica el algoritmo. Prohíbe no solo la extracción de archivos a ubicaciones fuera de la zona de pruebas (que es lo que se solicitó), sino también la creación de enlaces dentro de la zona de pruebas que apuntan a ubicaciones fuera de la zona de pruebas. Tengo curiosidad por saber si alguien puede colar archivos perdidos o enlaces.
import tarfile
from os.path import abspath, realpath, dirname, join as joinpath
from sys import stderr
resolved = lambda x: realpath(abspath(x))
def badpath(path, base):
# joinpath will ignore base if path is absolute
return not resolved(joinpath(base,path)).startswith(base)
def badlink(info, base):
# Links are interpreted relative to the directory containing the link
tip = resolved(joinpath(base, dirname(info.name)))
return badpath(info.linkname, base=tip)
def safemembers(members):
base = resolved(".")
for finfo in members:
if badpath(finfo.name, base):
print >>stderr, finfo.name, "is blocked (illegal path)"
elif finfo.issym() and badlink(finfo,base):
print >>stderr, finfo.name, "is blocked: Hard link to", finfo.linkname
elif finfo.islnk() and badlink(finfo,base):
print >>stderr, finfo.name, "is blocked: Symlink to", finfo.linkname
else:
yield finfo
ar = tarfile.open("testtar.tar")
ar.extractall(path="./sandbox", members=safemembers(ar))
ar.close()
Edición: a partir de python 2.7.4, esto no es un problema para los archivos ZIP: el método zipfile.extract()
prohíbe la creación de archivos fuera de la zona de pruebas:
Nota: Si un nombre de archivo de miembro es una ruta absoluta, se eliminarán las barras diagonales de unidad / UNC y las barras anteriores (atrás), por ejemplo:
///foo/bar
convierte enfoo/bar
en Unix, yC:/foo/bar
convierte enfoo/bar
en Windows. Y todos los componentes".."
en un nombre de archivo de miembro se eliminarán, por ejemplo:../../foo../../ba..r
convierte enfoo../ba..r
. En Windows, los caracteres no válidos (:
,<
,>
,|
,"
,?
Y*
) se reemplazan por un guión bajo (_).
La clase tarfile
no ha sido saneada de manera similar, por lo que la respuesta anterior sigue siendo válida.