
Consul Template validaba hacia dónde apuntaba un enlace simbólico durante la evaluación de la plantilla, pero su posterior obtención de dependencias leía la ruta original. Redirigir el enlace entre esas operaciones convirtió una referencia a un archivo dentro del sandbox en una divulgación de archivos fuera del sandbox.
Consul Template validaba a dónde apuntaba un enlace simbólico durante la evaluación de la plantilla, pero su posterior obtención de dependencias leía la ruta original. Reorientar el enlace entre esas operaciones convertía una referencia a un archivo dentro del sandbox en una divulgación de archivo fuera del sandbox.
Encontré este problema mientras revisaba HashiCorp Consul Template, con una pregunta de seguridad muy concreta en mente:
Si sandbox_path valida un enlace simbólico durante la evaluación de la plantilla, ¿la lectura posterior del archivo permanece vinculada a ese mismo objetivo validado?
En este caso, la respuesta fue no.
El asistente de plantilla file resolvía la ruta y aplicaba el sandbox configurado durante la evaluación de la plantilla. Pero después de esa comprobación, creaba una dependencia de archivo usando la ruta cruda original.
Esa dependencia se obtenía más tarde.
Si un atacante reorientaba el enlace simbólico en el intervalo entre la validación y la obtención de la dependencia, Consul Template podía leer un archivo fuera del sandbox. Si el enlace se restauraba antes de la siguiente renderización, la validación del sandbox volvía a pasar y el contenido externo almacenado en caché se seguía renderizando.
Ese problema se convirtió en CVE-2026-5061.
Boletín de HashiCorp: HCSEC-2026-12
Boletín de IBM: Boletín de seguridad CVE-2026-5061
0.42.0photo0
enlace simbólico dentro del sandbox controlado por el atacante -> la validación del sandbox resuelve a un objetivo seguro -> la dependencia almacena la ruta cruda original -> el atacante reorienta el enlace fuera del sandbox -> la obtención de la dependencia lee el archivo externo -> el atacante restaura el enlace seguro -> la siguiente validación pasa -> el contenido externo almacenado en caché se renderiza
Consul Template es una herramienta de renderizado de plantillas para datos de Consul y Vault.
Puede ejecutarse de forma continua, vigilar los cambios de las dependencias y renderizar datos en archivos o variables de entorno para que las aplicaciones los consuman.
El asistente de plantilla file lee un archivo local e inserta su contenido en la salida renderizada.
Dado que las lecturas de archivos locales pueden exponer secretos legibles por el proceso, Consul Template proporciona sandbox_path como límite para este asistente.
La propiedad de seguridad documentada es sencilla:
file deben estar dentro del sandbox configuradoEso convierte a sandbox_path en un límite de seguridad real.
La pregunta importante no era si la ruta parecía estar dentro del sandbox.
La pregunta real era:
¿El archivo que se lee sigue siendo el mismo archivo que pasó la validación del sandbox?
En las versiones vulnerables, no lo era.
Los sandboxes de sistemas de archivos suelen fallar en el intervalo entre la validación de la ruta y el acceso al archivo.
El patrón común es:
Esa suposición no es segura cuando un atacante puede modificar un enlace simbólico o una redirección equivalente del sistema de archivos entre las dos operaciones.
Consul Template hacía esta superficie especialmente interesante porque la evaluación de la plantilla y la obtención de dependencias eran etapas separadas.
Esa separación creaba la pregunta de seguridad adecuada:
¿La obtención de la dependencia está vinculada al objetivo validado, o vuelve a resolver la ruta controlada por el atacante?
Ese fue el límite en el que me centré.
La causa raíz era un desajuste de tiempo de comprobación frente a tiempo de uso entre:
template/funcs.godependency/file.goEn la revisión probada, fileFunc() hacía esto:
normalized := strings.TrimSpace(s)
err := pathInSandbox(sandboxPath, normalized)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(s)
El asistente de validación resolvía correctamente los enlaces simbólicos antes de comprobar la contención:
sandboxResolved, err := filepath.EvalSymlinks(filepath.Clean(sandbox))
targetResolved, err := filepath.EvalSymlinks(filepath.Clean(path))
rel, err := filepath.Rel(sandboxResolved, targetResolved)
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("'%s' is outside of sandbox", path)
}
Por lo tanto, la comprobación del sandbox entendía el objetivo resuelto.
Pero ese objetivo resuelto se descartaba.
NewFileQuery() almacenaba la cadena original en su lugar:
return &FileQuery{
stopCh: make(chan struct{}, 1),
path: s,
}, nil
Después, la obtención de la dependencia leía esa ruta de nuevo:
data, err := os.ReadFile(d.path)
Esa es toda la vulnerabilidad.
El código comprobaba una resolución del sistema de archivos y luego usaba otra distinta.
Porque un enlace simbólico es mutable.
El atacante no necesita hacer que un objetivo fuera del sandbox pase directamente pathInSandbox().
Solo necesita cambiar lo que la ruta cruda ya aprobada resuelve después de la validación, pero antes de que Fetch() realice la lectura.
La secuencia es:
pathInSandbox()FileQuery registra la ruta cruda del enlaceos.ReadFile(d.path) vuelve a resolver el enlaceEse último paso es importante.
Restaurar el enlace seguro no eliminaba el secreto ya obtenido de la caché de dependencias.
La distinción importante es la omisión de un control de seguridad explícito.
Esto no era simplemente:
"el archivo cambió mientras Consul Template lo estaba vigilando"
Vigilar los cambios de archivos es un comportamiento esperado.
El problema real era:
una ruta que pasaba la restricción documentada del sandbox podía usarse posteriormente para leer un archivo fuera de ese sandbox
Eso es un fallo directo del límite de confianza.
La aplicación ya había tomado una decisión de seguridad:
Pero la obtención posterior no estaba vinculada al objetivo que justificaba esa decisión.
Eso es lo que convirtió la mutabilidad ordinaria del sistema de archivos en una vulnerabilidad.
Construí un reproductor independiente alrededor de la secuencia exacta de evaluación y obtención de dependencias.
El entorno controlado usaba:
El flujo de reproducción fue:
file para que la validación del sandbox tenga éxito y la ruta cruda se registre como dependencia.os.ReadFile(d.path) lea a través del enlace redirigido.El comportamiento observado fue:
Esa comparación de hash era importante.
Demostraba que el valor final renderizado no era contenido seguro obsoleto, un artefacto del nombre de archivo ni un efecto secundario de una ruta de error.
Era el contenido exacto en bytes del archivo fuera del sandbox.
La prueba más sólida para un problema TOCTOU debe controlar la línea temporal.
Simplemente mostrar que un enlace simbólico puede apuntar fuera del sandbox sería más débil porque pathInSandbox() ya rechazaba un objetivo externo cuando lo observaba.
La afirmación de seguridad dependía de demostrar todos estos estados en orden:
Por eso el reproductor separaba explícitamente la validación de la plantilla, la obtención de la dependencia, la restauración del enlace y la siguiente renderización.
La PoC no dependía de tiempos aleatorios ni de adivinación repetida.
Impulsaba el ciclo de vida vulnerable de forma determinista.
Eso hacía mucho más fácil defender la causa raíz y el impacto.
Este problema requiere influencia local sobre el sistema de archivos.
Un atacante necesita acceso suficiente para crear, reemplazar o reorientar el enlace simbólico relevante o una ruta enlazada equivalente durante la ventana vulnerable.
El proceso de Consul Template también debe:
Esos requisitos importan.
Esto no era una lectura arbitraria de archivos remota y no autenticada desde cualquier implementación predeterminada.
Pero dentro del modelo de confianza local afectado, el impacto era significativo:
sandbox_pathEl riesgo aumenta cuando el proceso se ejecuta con acceso a credenciales sensibles, configuración de servicios, tokens o claves privadas.
HashiCorp clasificó el problema como:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
La puntuación base publicada es:
4.7 / Medio
Esa puntuación refleja las restricciones reales:
Esa es una clasificación razonable.
El problema es estrecho en cuanto a prerrequisitos del atacante, pero la omisión del sandbox y la divulgación de archivos resultante son ambas concretas.
El boletín de HashiCorp indica:
Afectadas: consul-template hasta 0.41.4
Corregida: consul-template 0.42.0
La corrección se distribuyó en la versión 0.42.0 el 15 de abril de 2026.
La corrección fue pequeña y abordó directamente el vínculo roto.
En lugar de validar el objetivo resuelto y luego construir la dependencia a partir de la entrada cruda, el código parcheado devuelve la ruta resuelta y se la entrega a NewFileQuery():
resolvedPath, err := resolveSandboxedPath(
sandboxPath,
strings.TrimSpace(s),
)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(resolvedPath)
La propiedad de seguridad cambió de:
a:
Eso cierra la ruta de reorientación de enlaces simbólicos reportada porque cambiar el enlace original ya no cambia la ruta almacenada por la dependencia.
El parche también añadió una prueba de regresión centrada en la secuencia exacta:
Ese es el tipo de remediación que quieres para este error:
Informé de este problema de forma privada a HashiCorp Security el 20 de marzo de 2026.
El informe incluía:
HashiCorp corrigió el problema en 0.42.0 y publicó HCSEC-2026-12 el 12 de mayo de 2026.
IBM publicó un boletín de seguridad correspondiente para el mismo CVE.
Ambos boletines oficiales reconocieron el informe como:
Mohamed Abdelaal (0xmrma)
La lección clave es sencilla:
validar una ruta no es suficiente si la operación de archivo posterior puede resolver esa ruta a un objeto diferente
Esa regla se aplica mucho más allá de Consul Template.
Importa en cualquier lugar donde el código haga:
La propiedad de seguridad más profunda no es:
"la cadena de ruta parecía segura una vez"
Es:
el objeto usado por la operación sensible debe ser el objeto que pasó la validación
En este caso, Consul Template validó una resolución y obtuvo otra.
Ese intervalo fue suficiente.
sandbox_path era un límite de seguridad explícito para archivos localespathInSandbox() resolvía y validaba correctamente el objetivo del enlace simbólicoFileQuery conservaba la ruta original influenciada por el atacanteos.ReadFile0.42.0 corrigió el error vinculando la dependencia a la ruta resuelta y validadaEsta vulnerabilidad no trataba de omitir una comprobación de prefijo de cadena.
Trataba de tiempo e identidad.
Consul Template comprobaba hacia dónde apuntaba el enlace durante la evaluación de la plantilla. La obtención de la dependencia volvía a hacer la misma pregunta al sistema de archivos más tarde. Un atacante podía cambiar la respuesta entre esos dos momentos.
Por eso esto se convirtió en CVE-2026-5061.
Corregido en consul-template 0.42.0.