wandres.dev
CÓMO FUNCIONA LA CACHÉ · hashing e invalidación

Seguridad de la caché: envenenamiento, CREEP y aislamiento por rama

El direccionamiento por contenido que hace compartible una caché es también lo que la vuelve envenenable: quien pueda escribir bajo una clave dicta lo que recibirá todo el que la calcule. Este nivel estudia el envenenamiento de caché, disecciona el CVE-2025-36852 —CREEP, severidad 9.4— donde una caché remota sin aislamiento por rama deja a un contribuidor de un fork envenenar artefactos que las ramas protegidas reutilizan y despliegan, y explica cómo el aislamiento branch-scoped de Nx Cloud cierra ese agujero.

⏱ 17 min

Una caché compartida es, antes que una optimización, una frontera de confianza. En cuanto varias máquinas y varios contribuidores escriben y leen del mismo almacén, la propiedad que la hacía maravillosa —que cualquiera que produzca el mismo contenido calcula la misma clave y puede pedir por ella— se convierte en su talón de Aquiles: si cualquiera que calcule esa clave puede también escribir bajo ella, entonces cualquiera puede dictar lo que recibirán todos los demás. Este nivel mira la caché desde la óptica del atacante, estudia el envenenamiento como clase de vulnerabilidad, y analiza el caso que en 2025 puso el tema en el mapa: CREEP.

🎯 Al terminar esta lección sabrás
  • Entender el envenenamiento de caché como un ataque sobre la escritura, no sobre la lectura.
  • Reconstruir el mecanismo del CVE-2025-36852 (CREEP) y su condición de carrera “el primero en cachear gana”.
  • Explicar por qué el envenenamiento en construcción esquiva el cifrado, los permisos y los checksums.
  • Justificar el aislamiento por rama (branch-scoped) como la defensa estructural del problema.

El envenenamiento de caché

El envenenamiento explota una suposición que casi ningún sistema de build hace explícita: que un hit es de fiar. Cuando la caché encuentra una entrada bajo la clave que calculaste, te entrega su contenido sin volver a verificarlo —ese es justo el ahorro que buscas—. Pero esa confianza tiene una cara oscura: si un atacante consigue colocar un artefacto malicioso bajo una clave legítima, todo consumidor que calcule esa clave recibirá el veneno con la misma naturalidad con la que habría recibido el resultado correcto, y sin sospechar nada, porque el hash cuadra.

Nótese dónde está la vulnerabilidad: no en la lectura, sino en la escritura. El direccionamiento por contenido garantiza que la clave describe fielmente las entradas, pero no dice nada sobre quién tuvo derecho a poblar esa clave. Un hash es una huella del contenido; no es una firma de su autor ni una prueba de su procedencia. Si el almacén no distingue entre un artefacto producido en un contexto de confianza y otro producido por un contribuidor cualquiera, la clave se convierte en un buzón abierto: el primero que escriba en él decide lo que leerá todo el mundo.

Y el problema se agrava justo por la virtud que hace útil la caché: la ausencia de reverificación. Un hit es rápido precisamente porque no vuelve a comprobar nada —confía en que lo guardado bajo esa clave es correcto y lo entrega—. Esa confianza es el motor del rendimiento y, a la vez, la puerta del ataque: cuanto más ciega es la lectura a la procedencia del dato, más barato es el hit y más devastador el envenenamiento. No se puede pedir a la lectura que sea a la vez instantánea y desconfiada; por eso la defensa tiene que vivir en otro sitio.

📝
Confianza implícita, el pecado original de las cachés compartidas

Casi todas las defensas clásicas de una caché protegen el dato en tránsito y en reposo: cifrado del transporte, control de acceso al bucket, checksums de integridad. Todas presuponen que lo que se guardó era legítimo y solo hay que evitar que se altere después. El envenenamiento ataca antes de ese punto: corrompe el artefacto en el momento de construirlo y subirlo, por los cauces autorizados del propio sistema. El dato viaja cifrado, con permisos correctos y con su checksum intacto. Simplemente, es veneno legítimamente firmado.

CVE-2025-36852: CREEP

En junio de 2025, una investigación divulgada por Nx describió y bautizó esta clase de fallo en cachés de build remotas: CREEP, siglas de Cache Race-condition Exploit Enables Poisoning, registrada como CVE-2025-36852 con una severidad crítica de 9.4. Su hallazgo incómodo: en muchas configuraciones, cualquier desarrollador con permiso para abrir un pull request podía inyectar código en los artefactos de producción.

El mecanismo encadena tres hechos que, por separado, parecen inofensivos. Primero, la caché remota se comparte entre contextos de confianza distinta: los pull requests —a menudo desde forks de colaboradores externos— y las ramas protegidas como main escriben y leen del mismo almacén. Segundo, la clave es un hash del contenido y no codifica la rama ni el nivel de confianza: un PR que parte del mismo estado de fuentes que main calcula exactamente la misma clave. Tercero, rige el principio de “el primero en cachear gana”: quien suba primero un artefacto para un estado dado de las fuentes fija la versión que se servirá en todas partes donde ese estado reaparezca, producción incluida.

flowchart TD
PR[PR de un fork no confiable] --> Hh[Calcula el hash que main usara]
Hh --> Up[Sube primero un artefacto envenenado]
Up --> Store[Cache remota compartida sin aislamiento]
Main[Rama protegida main] --> Same[Calcula el mismo hash]
Same --> Store
Store --> Hit[Cache hit: descarga el artefacto del atacante]
Hit --> Deploy[Despliega codigo envenenado]
style Deploy fill:#f38ba8,color:#11111b
style Store fill:#f9e2af,color:#11111b

El ataque, entonces, no necesita romper nada: usa el sistema tal como está diseñado. Un contribuidor malicioso abre un PR, precalcula el hash que la rama protegida usará para su build, produce un artefacto envenenado —un dist con una puerta trasera— y lo sube a la caché compartida bajo esa clave, ganando la carrera. Cuando main construye ese mismo estado de fuentes, calcula la clave idéntica, obtiene un hit y descarga el artefacto del atacante en lugar de generar el suyo. Lo que se despliega a producción es código que nadie revisó ni aprobó.

La divulgación subrayó dos cosas incómodas. La primera es el alcance: la configuración vulnerable —caché remota compartida entre PRs y ramas protegidas, sin partición por nivel de confianza— era el montaje por defecto o el más cómodo en incontables organizaciones, de modo que el fallo no era una rareza de laboratorio sino el estado natural de miles de pipelines. La segunda es que la carrera no exige un atacante sofisticado: como rige el “primero en cachear gana”, incluso un fork bienintencionado cuyo CI corriera antes que la rama protegida podía, sin malicia alguna, fijar el artefacto que producción reutilizaría. La severidad 9.4 no es alarmismo: refleja que el ataque es de bajo esfuerzo, no requiere privilegios especiales y desemboca en ejecución de código en producción.

⚠️
Por qué esquiva todas las defensas de siempre

Lo que hace a CREEP especialmente peligroso es que el envenenamiento ocurre en la fase de construcción del artefacto, antes de que actúe cualquier medida de tránsito o almacenamiento. El cifrado del transporte protege un dato ya envenenado; el control de acceso al bucket autoriza a un escritor que tenía permiso legítimo de PR; el checksum valida la integridad de un artefacto que ya nació corrupto. Ninguna de esas capas mira la pregunta que importa —¿en qué contexto de confianza se produjo esto?—, así que ninguna lo detiene. El fallo afecta sobre todo a las cachés basadas en buckets de objetos como S3 o GCS, y por extensión a cualquier arquitectura de caché con la misma forma.

El aislamiento por rama

La defensa no puede estar en la lectura —un hit tiene que ser rápido y no revalidar—, así que tiene que estar en la escritura y en la topología del almacén. La respuesta estructural es el aislamiento por rama (branch-scoped): particionar el espacio de claves según el nivel de confianza, de modo que un artefacto producido en un contexto no confiable jamás pueda ser leído por uno confiable bajo la misma clave.

En concreto, el aislamiento branch-scoped que Nx Cloud implementa como respuesta a CREEP separa los ámbitos: las corridas de PRs no confiables pueden leer de la caché compartida para acelerar —eso no entraña riesgo— pero sus escrituras quedan confinadas a un ámbito aislado, atado a esa rama o a ese contexto no confiable, del que las ramas protegidas nunca leen. Dicho de otro modo: un fork puede beneficiarse de lo que produjo la confianza, pero no puede inyectar nada en lo que la confianza consumirá. Se rompe la premisa segunda del ataque —que la clave no distinga contextos— añadiendo el contexto de confianza a la partición del almacén, aunque no al hash de contenido.

👁️

Lectura amplia

Un contexto no confiable puede leer de la caché compartida para acelerar sin comprometer a nadie.

🚧

Escritura confinada

Sus escrituras van a un ámbito aislado por rama del que las ramas protegidas nunca leen.

🔑

Confianza en la topología

El nivel de confianza se codifica en la partición del almacén, no en el hash de contenido.

🛡️

Producción intocable

Un PR de un fork ya no puede colocar nada bajo una clave que main vaya a consumir.

Qué comprobar en tu proveedor de caché

El aislamiento por rama no es una casilla que se marca y se olvida; conviene verificar que tu proveedor lo aplica de verdad y en la dirección correcta. Cuatro preguntas concretas separan una configuración segura de una expuesta:

  • ¿Las corridas de PRs desde forks escriben en un ámbito distinto del que leen las ramas protegidas, o comparten espacio de claves?
  • ¿Puede una rama protegida acabar leyendo un artefacto que produjo un contexto no confiable, aunque sea por una ruta indirecta?
  • ¿La partición por confianza es el comportamiento por defecto, o hay que activarla explícitamente en la configuración?
  • ¿Los tokens de escritura a la caché tienen alcance por contexto, de modo que un token de PR no pueda escribir donde escribe la rama protegida?

Si alguna respuesta te deja dudando, asume lo peor y trata tu caché remota como envenenable hasta demostrar lo contrario. La comodidad por defecto de “todos comparten todo” es exactamente la trampa que CREEP explotó a escala.

La lección de diseño trasciende a la herramienta concreta. Toda caché compartida entre partes de confianza distinta necesita responder, explícitamente, a la pregunta de quién puede escribir qué y quién puede leer de dónde. Si esa respuesta se deja implícita —“todos comparten todo porque el hash ya garantiza que el contenido cuadra”—, el direccionamiento por contenido, que resolvía el problema de la reutilización, deja abierto de par en par el problema de la procedencia.

Más allá de CREEP: la superficie de ataque

CREEP es el ejemplar más nítido de una familia más amplia. En cuanto una caché se comparte, hereda todas las vías por las que un contenido no confiable puede acabar en un lugar confiable, y el aislamiento por rama cierra la puerta principal pero no todas las ventanas. Conviene tener el mapa completo, porque cada vector se defiende de una manera distinta.

  • Secretos en logs o artefactos cacheados. Si una tarea imprime un token o lo escribe en un archivo declarado como output, ese secreto queda congelado bajo una clave y se reproduce en cada hit, potencialmente en un almacén compartido. La caché convierte una fuga puntual en una fuga persistente y distribuida.
  • Plugin o herramienta de build comprometido. Un plugin malicioso puede escribir archivos arbitrarios en el artefacto que luego se cachea y se distribuye al equipo entero con la bendición implícita del hash.
  • Dependencia interna envenenada río arriba. Si el build de una librería interna produce salida maliciosa, su artefacto cacheado se reutiliza aguas abajo sin que nadie vuelva a mirarlo, propagándose por el grafo.
  • La caché como canal de exfiltración. Un almacén compartido con permisos laxos de lectura y escritura puede servir para sacar datos entre contextos que jamás debieron comunicarse.
Vector Riesgo Mitigación
secreto en log u output fuga persistente y distribuida no imprimas ni caches secretos; sanea la salida
plugin comprometido artefacto con puerta trasera fija versiones y audita lo que ejecutas
dependencia interna envenenada veneno reutilizado río abajo aislamiento y procedencia por contexto
almacén compartido laxo exfiltración de datos acota permisos de lectura y escritura
📝
La caché no crea amenazas nuevas; amplifica las de siempre

Ninguno de estos vectores es exclusivo del caching: un secreto en un log, un plugin comprometido o una dependencia envenenada son problemas de seguridad clásicos. Lo que hace la caché es amplificarlos, porque toma un evento puntual —una corrida, una fuga— y lo congela bajo una clave para reproducirlo, idéntico, en cada máquina que acierte esa clave. Por eso la defensa correcta es la de siempre en profundidad —no imprimir secretos, fijar y auditar dependencias, acotar permisos— más la capa que CREEP añadió al vocabulario común: aislar por confianza el almacén que todos comparten.

El hash prueba qué es un artefacto, jamás quién tuvo derecho a crearlo

Aquí se toca uno de los límites más profundos y menos comprendidos del direccionamiento por contenido, y vale la pena enunciarlo con toda su fuerza: un hash es una prueba de identidad del contenido, y absolutamente nada más. Certifica, con la solidez de la criptografía, que estos bytes son estos bytes; no dice ni una palabra sobre quién los produjo, en qué contexto, con qué autoridad ni si merecían existir bajo esa clave. Toda la magia de la caché compartida que estudiamos en los niveles anteriores descansa sobre esa propiedad —que la clave viaja con el contenido y cualquiera que lo reproduzca la recalcula—, pero esa misma propiedad, girada un cuarto de vuelta, es exactamente el agujero que CREEP atraviesa: si la clave solo habla del contenido, entonces la clave no puede impedir que un contenido malicioso, calculado por quien no debía, ocupe el lugar del legítimo. La reutilización y la procedencia son dos problemas distintos, y el hash resuelve brillantemente el primero mientras es, por construcción, ciego al segundo. El error de toda una generación de cachés remotas fue confundirlos: creer que porque el hash garantizaba la corrección del contenido, garantizaba también la legitimidad de su autor. No lo hace, no puede hacerlo, y tratar de que lo haga —metiendo la identidad del autor dentro del hash— rompería el direccionamiento por contenido que hacía útil todo el sistema, porque entonces el mismo contenido producido por dos personas tendría dos claves y la reutilización se evaporaría. La solución correcta, la que enseña CREEP, es no pedirle al hash lo que no puede dar y añadir la confianza en otra capa: partir el almacén por contexto, de modo que el ámbito desde el que se escribe herede la autoridad de quien escribe. Grábate el principio, porque gobierna toda caché que cruce una frontera de confianza: el direccionamiento por contenido responde qué es esto con certeza absoluta, y guarda un silencio igual de absoluto sobre quién tuvo derecho a ponerlo ahí. Quien confunde esas dos preguntas construye una caché que un desconocido puede usar para desplegar a producción.

⚔️ Piensa como quien envenena una caché
  1. Dibuja tu topología de caché remota e identifica quién tiene permiso de escritura: ¿escriben los PRs desde forks en el mismo almacén que la rama protegida?
  2. Comprueba si tu clave de caché codifica de algún modo la rama o el contexto de confianza, o si un PR y main comparten clave para el mismo estado de fuentes.
  3. Razona el ataque “el primero en cachear gana” sobre tu propia configuración: ¿podría un contribuidor precalcular la clave que usará producción y ganar la carrera?
  4. Verifica que tu proveedor aplica aislamiento por rama —lectura amplia, escritura confinada— y que las ramas protegidas nunca leen artefactos escritos por contextos no confiables.
  5. Enuncia con tus palabras por qué el cifrado, los permisos del bucket y los checksums no habrían detenido a CREEP, y qué capa sí lo hace.