
Prueba de concepto para CVE-2026-93349, una inyección de comandos del sistema operativo en Frictionless <= 5.20.0rc1 explore CLI a través de rutas de descriptor de Data Package maliciosas.
<= 5.20.0rc1 explore de FrictionlessSeveridad: Alta, CVSS 3.1 8.8
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Afecta a: todas las versiones de Frictionless <= 5.20.0rc1
Componente: frictionless.console.commands.explore
CWE: CWE-78 (Inyección de comandos del sistema operativo), CWE-20 (Validación de entrada incorrecta)
Reportado por: Sai Teja Erukude
Las versiones de Frictionless hasta la versión preliminar 5.20.0rc1 inclusive contienen una
vulnerabilidad de inyección de comandos del sistema operativo en el comando CLI frictionless explore.
El comando carga un descriptor de Data Package proporcionado por el usuario, recopila las rutas
de los recursos, deriva resource.normpath, une esas rutas en una única cadena de comando
y pasa esa cadena directamente a os.system:
paths = [resource.normpath for resource in resources if resource.normpath]
os.system(f"vd {' '.join(paths)}")
Debido a que resources[*].path es metadato controlado por el descriptor proveniente de
datapackage.json, un atacante puede incluir metacaracteres de shell en la ruta de un recurso.
Cuando una víctima ejecuta frictionless explore sobre el descriptor malicioso, el
shell interpreta esos metacaracteres y ejecuta el comando inyectado.
Un atacante puede ejecutar comandos arbitrarios en el contexto de seguridad del usuario
que ejecuta frictionless explore.
Esto puede afectar a flujos de trabajo en los que los usuarios inspeccionan, validan o exploran descriptores de Data Package provenientes de proyectos, archivos, repositorios o conjuntos de datos compartidos no confiables.
Dependiendo del entorno local, esto puede permitir:
La ruta vulnerable es el comando explore:
frictionless explore <package>
-> Package.from_descriptor(...)
-> Resource.list()
-> resource.normpath
-> os.system(f"vd {' '.join(paths)}")
El PoC genera un descriptor de Data Package en el que resources[0].path contiene una
carga útil inofensiva que escribe un archivo marcador.
En Windows, la ruta generada utiliza separadores de comando &:
data.csv & echo frictionless_explore_command_injection_triggered>"...\marker.txt" & rem
En sistemas POSIX, la ruta generada utiliza separadores de comando de punto y coma:
data.csv; printf %s frictionless_explore_command_injection_triggered > .../marker.txt; #
El ejecutor invoca el comando real frictionless explore del paquete instalado. Limpia
PATH para el proceso hijo para que el visor vd previsto no se lance de forma interactiva,
pero el comando inyectado aún se ejecuta cuando el shell analiza la cadena de comando construida.
package_builder.py crea el descriptor de Data Package controlado por el atacante
y un archivo CSV benigno.
run_poc.py valida el descriptor, invoca
frictionless explore e imprime la evidencia de ejecución.
Ejecutar únicamente en un entorno de prueba local:
python -m venv .venv
.venv\Scripts\activate
python -m pip install -r requirements.txt
python -B run_poc.py
El archivo requirements.txt incluido fija frictionless==5.20.0rc1, la última
versión preliminar vulnerable confirmada en este rango.
Evidencia esperada en versiones vulnerables:
frictionless_version : 5.20.0rc1
installed_package : True
descriptor_valid : True
listed_resource_count : 1
raw_path_contains_payload : True
normpath_contains_payload : True
explore_returncode : 0
viewer_launch_disabled : True
marker_exists : True
marker_contents
frictionless_explore_command_injection_triggered
success : True
La evidencia importante es que el descriptor malicioso es válido, la ruta
controlada por el descriptor sobrevive hasta resource.normpath, y marker.txt
se crea después de que se ejecuta frictionless explore.
La carga útil es intencionalmente inofensiva. Solo escribe esta cadena de marcador local:
frictionless_explore_command_injection_triggered
No genera una sesión de shell, no se conecta a un servicio de red, no lee secretos, no elimina datos ni modifica archivos fuera del directorio del PoC.
Evite pasar rutas controladas por el descriptor a un shell.
Reemplace la invocación basada en shell con subprocess.run utilizando un vector
de argumentos:
subprocess.run(["vd", *paths], check=False)
Esto pasa cada ruta de recurso como un argumento literal a vd. Los metacaracteres
de shell dentro de una ruta permanecen como caracteres ordinarios dentro de esa única entrada
de argv y no se ejecutan.
Incluir en listas negras caracteres específicos de rutas es más débil porque la sintaxis del shell varía entre plataformas y shells. El límite de invocación del comando es la causa raíz, por lo que la solución duradera debería evitar la interpretación del shell.
Consulte fix.md para ver un parche sugerido.
Descubierto y reportado por Sai Teja Erukude, coordinado a través de VulnCheck.
exploit_walkthrough.mdfix.md