Publicado el 5 min de lectura

Comprueba las rutas propuestas por IA antes de escribir con Python

En esta página

Un asistente de IA puede proponer guardar un informe como reports/summary.md. Antes de que una herramienta escriba nada, comprueba adónde conduce realmente ese nombre. Una ruta anidada puede atravesar un enlace simbólico hacia otra carpeta, aunque su escritura parezca inofensiva.

Este tutorial crea un comprobador de destinos de solo lectura y prueba nueve propuestas sintéticas. Complementa una vista previa de renombrados sencillos: aquí permitimos destinos anidados para informes, no solo nombres de archivo sin directorios. Usa Python 3.11 o posterior en Linux o macOS; no necesitas una cuenta de IA ni paquetes externos.

Una carpeta de proyecto dentro de un límite transparente, con una ruta verde azulada aprobada para documentos y una ruta coral detenida por una barrera.

1. Define el contrato de los destinos

La aplicación, no el modelo, elige una carpeta privada de salida. Pide al asistente rutas relativas con letras ASCII minúsculas, dígitos, guiones bajos y guiones, separadores de carpeta con barra normal y extensión final .md. Limita cada ruta a 200 caracteres. Los directorios superiores deben existir; los nombres ocupados se rechazan.

Esto excluye deliberadamente espacios, letras acentuadas, rutas absolutas y ... MITRE describe salir de un directorio restringido mediante rutas procedentes de entradas externas como recorrido de rutas. Rechaza los nombres no admitidos en lugar de repararlos silenciosamente.

Si las propuestas llegan en JSON, valida primero el JSON y después pasa cada cadena de ruta a esta comprobación independiente.

2. Guarda el comprobador de solo lectura

Guarda esto como check_paths.py en un directorio de trabajo nuevo:

from pathlib import Path
import re


def checked_destination(root, raw):
    if (
        not isinstance(raw, str)
        or len(raw) > 200
        or not re.fullmatch(r"[a-z0-9_-]+(?:/[a-z0-9_-]+)*\.md", raw)
    ):
        raise ValueError("format")
    root = root.resolve(strict=True)
    candidate = root / raw
    try:
        target = candidate.resolve(strict=False)
    except (OSError, RuntimeError) as error:
        raise ValueError("unresolvable") from error
    if not target.is_relative_to(root):
        raise ValueError("outside root")
    if not target.parent.is_dir():
        raise ValueError("missing parent")
    if target.exists() or candidate.is_symlink():
        raise ValueError("occupied")
    return target

La lista de caracteres permitidos comprueba la escritura. La referencia de resolve() de Python explica que la resolución sigue enlaces simbólicos; strict=False permite un destino que aún no existe. Resuelve primero porque is_relative_to() por sí solo es léxico y no inspecciona enlaces.

No sustituyas la comprobación de pertenencia por str(target).startswith(str(root)): una carpeta hermana llamada output-backup comparte el prefijo output. La comprobación de ocupación también rechaza enlaces simbólicos finales rotos, que no apuntan a un archivo existente.

3. Crea datos de prueba desechables

Guarda esto como demo_paths.py junto al comprobador. Solo crea carpetas temporales de prueba, un archivo testigo y dos enlaces simbólicos. El enlace escape apunta a la carpeta hermana output-backup; broken.md apunta a un archivo inexistente.

from pathlib import Path
from tempfile import TemporaryDirectory
from check_paths import checked_destination

with TemporaryDirectory(prefix="ai-paths-") as directory:
    base = Path(directory).resolve()
    root, outside = base / "output", base / "output-backup"
    root.mkdir()
    outside.mkdir()
    (root / "reports").mkdir()
    (root / "existing.md").write_text("keep\n", encoding="utf-8")
    (root / "escape").symlink_to(outside, target_is_directory=True)
    (root / "broken.md").symlink_to(root / "gone.md")
    before = sorted(str(p.relative_to(base)) for p in base.rglob("*"))
    cases = [
        ("nested", "reports/summary.md", "ALLOW"),
        ("parent", "../outside.md", "BLOCK format"),
        ("absolute", str(outside / "note.md"), "BLOCK format"),
        ("symlink", "escape/leak.md", "BLOCK outside root"),
        ("existing", "existing.md", "BLOCK occupied"),
        ("extension", "reports/summary.txt", "BLOCK format"),
        ("backslash", r"reports\summary.md", "BLOCK format"),
        ("missing-parent", "new/summary.md", "BLOCK missing parent"),
        ("broken-link", "broken.md", "BLOCK occupied"),
    ]
    for label, raw, expected in cases:
        try:
            target = checked_destination(root, raw)
            assert target == root / "reports/summary.md"
            result = "ALLOW"
        except ValueError as error:
            result = f"BLOCK {error}"
        assert result == expected, (label, result)
        print(f"{label}: {result}")
    assert before == sorted(str(p.relative_to(base)) for p in base.rglob("*"))
    assert (root / "existing.md").read_text(encoding="utf-8") == "keep\n"
    print("PASS: 9 cases; no proposed files written")

Las aserciones comprueban cada decisión, comparan el inventario de directorios antes y después de la validación y confirman que el contenido del archivo testigo no cambia. El directorio temporal se elimina automáticamente.

4. Ejecuta y valida

Ejecuta normalmente, sin la opción -O de Python, que desactiva las aserciones:

python3 demo_paths.py

Salida esperada:

nested: ALLOW
parent: BLOCK format
absolute: BLOCK format
symlink: BLOCK outside root
existing: BLOCK occupied
extension: BLOCK format
backslash: BLOCK format
missing-parent: BLOCK missing parent
broken-link: BLOCK occupied
PASS: 9 cases; no proposed files written

Probado localmente con Python 3.11.4 en Linux: código de salida 0, stderr vacío y ningún archivo de prueba restante. Los casos negativos son pruebas satisfactorias del rechazo, no ejecuciones fallidas.

El caso del enlace simbólico es el más importante: su entrada cumple la regla de nombres, pero al resolverla sale de la raíz aprobada. Aceptar el caso anidado significa que ese destino cumple la política en el momento de la inspección. No certifica el contenido del informe ni autoriza ejecutar código escrito por el modelo.

5. Resuelve problemas y separa la ejecución

  • format: solicita un nombre relativo que cumpla las reglas. No elimines componentes de recorrido ni decodifiques el valor después de validarlo.
  • outside root: inspecciona el directorio superior enlazado; no amplíes la raíz permitida para que la prueba pase.
  • missing parent: deja que código de confianza de la aplicación prepare el subdirectorio previsto y vuelve a validar.
  • occupied: elige otro nombre; no borres la entrada existente.
  • Falla la creación de enlaces simbólicos: usa un sistema de archivos local que los permita. Windows requiere pruebas específicas de plataforma.

Esta es una comprobación puntual, no un entorno aislado de archivos inmune a condiciones de carrera. Otro proceso podría reemplazar un directorio entre la validación y la escritura: una condición de carrera entre comprobación y uso. Un futuro escritor necesita permisos aislados, controles de apertura de archivos resistentes a estas carreras y un comportamiento que impida sobrescribir. Para propuestas por lotes, rechaza también destinos resueltos duplicados antes de escribir nada.

Idea práctica

Deja que la IA proponga destinos mientras el código de la aplicación controla la raíz y comprueba cada ruta. Combina esta vista previa con una lista de campos permitidos, separa la ejecución y añade un caso de prueba cada vez que aparezca un fallo nuevo.

Artículos relacionados

Últimos artículos