Tema representativo de entrevista

Entrevista de código en Go: ¿Cómo manejarías de forma segura nombres de archivo no confiables con os.Root?

CodingIntermedio
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Un extractor de archivos comprimidos recibe nombres de archivo no confiables y debe mantener cada salida dentro de un único directorio. Usando Go 1.24+, diseña la implementación segura y explica .., symlinks, TOCTOU, limpieza de recursos y límites multiplataforma.

Prompt y alcance

Un extractor de archivos comprimidos escribe las cargas de los usuarios en /srv/uploads/job-42. Los nombres de las entradas del archivo están totalmente controlados por el llamador, quien puede enviar ../../etc/passwd, una ruta absoluta o un symlink que apunte fuera del directorio de trabajo. El servicio debe crear directorios y archivos asegurándose de que cada operación permanezca dentro del directorio de trabajo.

Usa Go 1.24 o posterior. Asume privilegios de aplicación ordinarios y Linux en producción, con posibles clientes Windows o WASI. Distingue un nombre de archivo no confiable de un llamador al que se le permite explícitamente elegir cualquier directorio de salida.

Lo que el entrevistador está evaluando

  • Si defines la raíz y la entrada controlada por el atacante antes de elegir una API restringida por directorio en lugar de hacer un reemplazo de cadenas.
  • Si sabes que os.Root rechaza .. y symlinks que escapan de la raíz, y en qué se diferencia esa garantía de filepath.Clean.
  • Si reconoces la ventana de TOCTOU al verificar con EvalSymlinks y abrir después.
  • Si manejas Root, descriptores de archivo, archivos temporales, limpieza y políticas de permisos.
  • Si mencionas los límites relacionados con montajes bind, GOOS=js, implementaciones de WASI y rutas muy profundas.

Preguntas para aclarar primero

  1. ¿La operación es de lectura, escritura o ambas? El acceso de solo lectura y la creación necesitan diferentes flags y permisos de OpenFile.
  2. ¿La raíz la fija el servicio o la elige el llamador? Si el llamador puede elegir cualquier directorio, os.Root no es un límite de sandbox adicional.
  3. ¿Puede el archivo comprimido crear symlinks, hard links, archivos de dispositivo o cambios de nombre? Los tipos no permitidos deben ser rechazados por la política del archivo en lugar de delegarse a Root.
  4. ¿Están dentro del alcance los destinos GOOS=js o WASI? El material oficial describe garantías de seguridad de rutas diferentes a las de las implementaciones basadas en descriptores de Unix.

Estructura de respuesta en 30 segundos

“Hago que el directorio de trabajo sea la única raíz. Cada nombre del archivo comprimido se pasa como un nombre relativo a un os.Root; no lo uno con una cadena base para llamar al os.Create ordinario. La creación, apertura y eliminación se realizan a través de los métodos de Root, por lo que .. y los symlinks que escapan fallan. Escribo a través de un nombre temporal con permisos restringidos y luego hago el commit según la política del producto. Las pruebas cubren traversal, carreras de symlinks, creación concurrente, nombres de dispositivos de Windows y el cierre de la raíz. Root restringe rutas, pero no reemplaza las comprobaciones de tipos de archivo, el aislamiento de privilegios ni los controles de montajes bind.”

Análisis detallado paso a paso

1. Fijar el invariante de seguridad

“El nombre no contiene ..” no es una definición de seguridad. Un atacante puede usar un symlink o reemplazar una entrada de directorio entre una verificación y una apertura. Define el invariante con precisión: cada operación del sistema de archivos derivada de un nombre de archivo comprimido se resuelve dentro de la raíz de trabajo, y la operación falla cuando eso no se puede demostrar.

El os.OpenRoot de Go 1.24 abre un directorio raíz. Métodos como Root.Open, Root.Create, Root.OpenFile, Root.Mkdir y Root.Stat aceptan nombres relativos a esa raíz. La implementación rechaza .. y el recorrido de symlinks fuera de la raíz. Reutiliza una única raíz para la tarea y ciérrala después de que todos los descriptores de archivo estén cerrados.

go
func writeEntry(rootDir, name string, data []byte) error {
    root, err := os.OpenRoot(rootDir)
    if err != nil {
        return err
    }
    defer root.Close()

    f, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
    if err != nil {
        return err
    }
    defer f.Close()

    _, err = f.Write(data)
    return err
}

Esto evita que la resolución de nombres escape, pero el código de producción aún necesita límites en el tamaño de los archivos, el conteo de entradas y la profundidad de los directorios, además de la eliminación de salidas incompletas tras un fallo de escritura. O_EXCL significa “no sobrescribir un archivo existente”; no es deduplicación de archivos comprimidos ni idempotencia de negocio.

2. Explicar por qué la sanitización de cadenas es insuficiente

filepath.Clean puede normalizar a/../b, pero no puede probar que a no sea un symlink fuera de la raíz. Verificar con EvalSymlinks y abrir después sigue teniendo una ventana de TOCTOU: una entrada de directorio puede cambiar después de la verificación. filepath.Join seguido de os.Create no proporciona ningún límite de directorio.

En Unix, Root utiliza un descriptor de directorio raíz y aperturas relativas restringidas, por lo que el límite participa en la operación en lugar de depender de una verificación separada. Las rutas relativas y los symlinks internos a la raíz están permitidos; ../ o un symlink absoluto que escape deben fallar. El llamador aún debe rechazar las entradas de symlink en el archivo a menos que el producto las admita explícitamente.

3. Manejar tipos de archivo y escrituras

Lee el tipo, tamaño, permisos y nombre de cada entrada antes de escribir. Rechaza archivos de dispositivo, FIFOs, hard links y symlinks que el producto no admita. Crea directorios padre con Root.Mkdir o MkdirAll, y luego crea archivos regulares con Root.OpenFile. Asigna presupuestos para el total de bytes, bytes por archivo, componentes de ruta y tareas concurrentes.

Una secuencia de escritura más segura es “nombre temporal → escritura completa → verificar → commit atómico”. El nombre temporal también debe crearse mediante Root; escribir en el directorio temporal del sistema y moverlo entre directorios altera las suposiciones de permisos, montaje y atomicidad. Si la API Root disponible no puede proporcionar el comportamiento de renombrado requerido, haz que sea una restricción documentada de la plataforma o del producto en lugar de recurrir silenciosamente a operaciones de ruta sin restricciones.

4. Definir los límites de plataforma y privilegios

El material de Go indica que las implementaciones de Unix normalmente rastrean el descriptor del directorio raíz, por lo que una raíz renombrada sigue haciendo referencia al directorio original. Windows usa un handle y bloquea ciertos nombres de dispositivo reservados. GOOS=js carece de la familia de llamadas openat, dejando un límite de TOCTOU en la validación de symlinks; la garantía de WASI depende de su implementación. Root tampoco bloquea montajes bind de Linux, archivos especiales de /proc ni el acceso a archivos de dispositivo en Unix.

El aislamiento de contenedores, la política de montajes, los privilegios de proceso y una lista de permisos de tipos de archivo son, por lo tanto, controles independientes. No describas Root como un sandbox de contenedor completo ni trates un directorio arbitrario seleccionado por el llamador como una raíz restringida.

5. Construir verificación ejecutable

Prueba ../escape, rutas absolutas, symlinks internos a la raíz, symlinks fuera de la raíz, nombres que terminan en ../, creación concurrente de un solo archivo, entradas sobredimensionadas y llamadas tras cerrar la raíz. Go 1.24.3 solucionó un caso en el que una ruta Root que terminaba en ../ podía abrir el directorio padre, por lo que CI debe fijar un toolchain que contenga la corrección y conservar esa prueba de regresión.

En Linux, usa directorios temporales y symlinks reales tanto para los casos aceptados como para los rechazados. Usa -race para condiciones de carrera de estado compartido, pero no lo consideres como prueba de un límite en el sistema de archivos. Un binario compilado de forma cruzada no prueba que existan las mismas garantías en el kernel; incluye cada GOOS en la matriz de pruebas.

Respuesta de muestra de alta calidad

“Hago que el directorio de trabajo sea una raíz fija y permito nombres de archivo comprimido no confiables solo como rutas relativas pasadas a os.Root. El extractor rechaza archivos de dispositivo, hard links y symlinks no admitidos, y limita las entradas, los bytes y la profundidad del directorio. Crea directorios padre y archivos regulares mediante Root, escribe en un nombre temporal bajo la misma raíz y elimina la salida incompleta en caso de fallo. .. y los symlinks que escapan son rechazados por la propia apertura restringida, evitando una ventana TOCTOU de verificar-y-luego-abrir.

No llamaría a esto un sandbox completo: los montajes bind, los privilegios, los montajes de contenedores y la política de archivos siguen siendo independientes. Las pruebas en Linux utilizan symlinks reales y escrituras concurrentes para traversal, creación duplicada, limpieza y ../ final; Windows, WASI y GOOS=js reciben pruebas de límites explícitas. Si el producto permite un directorio arbitrario seleccionado por el llamador, elimino la suposición falsa de Root y uso una política explícita de auditoría y privilegios.”

Errores comunes

  • Síntoma: filepath.Join(base, name) seguido del os.Create ordinario → Por qué falla: .. y los symlinks pueden resolverse fuera de base → Solución: fija la raíz y realiza cada operación mediante Root.
  • Síntoma: verificar con EvalSymlinks y luego abrir normalmente → Por qué falla: persiste una ventana de TOCTOU → Solución: haz que la restricción forme parte de la apertura y prueba el límite frente a condiciones de carrera.
  • Síntoma: afirmar que os.Root bloquea cualquier escape del sistema de archivos → Por qué falla: los montajes bind, los archivos de dispositivo y los privilegios están fuera de la garantía de esa API → Solución: añade controles de montajes, privilegios y tipos de archivo.
  • Síntoma: ignorar GOOS=js, WASI y versiones de parche → Por qué falla: las implementaciones de plataforma y las correcciones de seguridad difieren → Solución: fija el toolchain y construye una matriz de regresión multiplataforma.
  • Síntoma: probar solo la extracción exitosa → Por qué falla: la seguridad de rutas se demuestra con escapes rechazados y limpieza → Solución: prueba traversal, nombres absolutos, symlinks, conflictos, límites y llamadas con la raíz cerrada.

Preguntas de seguimiento y respuestas

Pregunta de seguimiento 1: ¿Se debe permitir un symlink interno a la raíz en un archivo comprimido?

Decídelo según los requisitos del producto. Si los enlaces deben preservarse, verifica mediante Root que el destino permanezca dentro de la raíz y limita el número y la profundidad de los enlaces. Si la tarea solo extrae archivos regulares, rechazar symlinks es más fácil de auditar. De cualquier manera, Root no reemplaza una lista de permisos de tipos de archivo.

Pregunta de seguimiento 2: ¿Cómo manejas los montajes bind de Linux?

Root no proporciona aislamiento contra montajes bind. Coloca el directorio de trabajo en un sistema de archivos dedicado mediante la política de montajes del contenedor o del host, restringe los privilegios de la aplicación y rechaza montajes no confiables en las comprobaciones de despliegue. Si el modelo de amenazas incluye un atacante con privilegios, usa un sandbox más robusto o un worker aislado en lugar de añadir más comprobaciones de cadenas.

Pregunta de seguimiento 3: ¿Por qué no escribir un archivo temporal en el directorio temp del sistema y moverlo a la raíz?

Un movimiento entre directorios cambia las suposiciones de permisos, montaje y atomicidad, y el archivo temporal podría quedar huérfano. Crea el nombre temporal mediante Root y completa la escritura y el commit bajo la misma raíz. Si la plataforma no puede proporcionar la operación atómica requerida, documenta una funcionalidad reducida o una implementación de compatibilidad auditada en lugar de volver silenciosamente a las uniones de rutas ordinarias.

Pregunta de seguimiento 4: ¿Se puede afirmar que GOOS=js tiene la misma seguridad?

No. La explicación oficial indica que ese destino carece de la familia openat y tiene una limitación de TOCTOU en la validación de symlinks. Restringe la funcionalidad para ese destino, confía en el sandbox del entorno de ejecución o mueve el procesamiento de archivos no confiables a un servidor con restricciones de directorio más estrictas. La API y la documentación deben dejar clara la diferencia.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta