wandres.dev
ISOLATES · no son contenedores

Seguridad del sandbox

Cómo V8 aísla a miles de inquilinos no confiables dentro de un mismo proceso, por qué la memory-safety no basta frente a Spectre y los canales laterales, y las capas con las que Cloudflare hace que el modelo sea seguro por diseño: memoria segura, capacidades sin autoridad ambiental, relojes cegados, concurrencia controlada, aislamiento dinámico de proceso y parcheo instantáneo de toda la flota.

⏱ 18 min

Aquí llega la pregunta incómoda que todo el nivel venía preparando. Si miles de inquilinos que no confían entre sí comparten un mismo proceso, y ya no hay un hipervisor ni un kernel invitado separándolos, ¿qué impide de verdad que el código de uno lea la memoria de otro? La respuesta no es una única muralla, sino un modelo de capas donde cada una cubre lo que la anterior no puede. Y en el centro hay un problema que la seguridad de memoria, por sí sola, no resuelve: los ataques de canal lateral.

🎯 Al terminar esta lección sabrás
  • Entender por qué un lenguaje con memoria segura es la primera frontera entre inquilinos.
  • Ver el modelo de capacidades: acceso solo por bindings, sin autoridad ambiental.
  • Comprender por qué Spectre y los canales laterales rompen las fronteras lógicas sin violarlas.
  • Conocer las capas con que Cloudflare mitiga esos ataques y qué significa seguro por diseño.

La primera frontera: un lenguaje con memoria segura

El isolate se apoya en un hecho del lenguaje: JavaScript y WebAssembly no permiten fabricar un puntero arbitrario ni leer fuera de las estructuras que el motor entrega.

Sin aritmética de punteros, no existe la operación “lee la dirección de memoria X” que en C o en ensamblador te dejaría hurgar en el heap del vecino. La frontera entre inquilinos es, en primera instancia, una propiedad demostrable del lenguaje ejecutado por V8.

Y no es un motor cualquiera: es el mismo que aísla código no confiable en miles de millones de navegadores cada día. Es, con diferencia, el sandbox de software más probado del planeta, y Cloudflare hereda todo ese endurecimiento acumulado.

WebAssembly refuerza esa base por su cuenta: su memoria es un bloque lineal acotado, y un módulo no puede direccionar ni un byte fuera de él. Aun cuando lleves al edge código escrito en un lenguaje sin memoria segura, al compilarlo a Wasm queda encerrado en esa caja.

A esa garantía se suma un modelo de capacidades.

Un Worker no tiene autoridad ambiental: no puede tocar nada que no le hayan entregado explícitamente. Es el principio del menor privilegio llevado al runtime: por defecto, acceso cero.

Sus únicas puertas al exterior son los bindings que recibe en env y la salida de red por fetch. No hay credenciales globales flotando en variables de entorno del sistema ni acceso implícito a otros recursos. Si un recurso no está en tu env, para tu Worker sencillamente no existe.

export default {
  async fetch(request, env, ctx) {
    // El Worker solo alcanza lo que env le concede explicitamente.
    // No hay forma de tocar la base de datos de otro inquilino:
    // ese binding sencillamente no existe en este env.
    const valor = await env.MI_KV.get("clave");
    return new Response(valor ?? "sin datos");
  },
};

Ese env es la lista completa de lo que el Worker puede alcanzar. No hay una puerta trasera global, ni un sistema de ficheros compartido, ni un catálogo de servicios que enumerar: lo que no te dieron, no existe para ti.

Enumerar la superficie de ataque de un Worker se reduce, así, a leer su env. Es una propiedad rara y valiosa: la seguridad se vuelve legible.

Y esa legibilidad no es cosmética. Una base de cómputo confiable que cabe en la cabeza de un equipo se audita de verdad; una hecha de capas opacas de virtualización solo se supone segura.

El problema que la memory-safety no cubre: Spectre

Si todo se redujera a la seguridad de memoria, el nivel terminaría aquí. Pero existe una clase de ataques que rompe fronteras sin violar el modelo de memoria del lenguaje: los canales laterales microarquitectónicos, encabezados por Spectre.

La CPU, para ir rápida, ejecuta instrucciones de forma especulativa y comparte cachés entre todo lo que corre en ella.

Un atacante puede inducir a la CPU a acceder especulativamente a memoria que no debería, y luego inferir su contenido midiendo diferencias de tiempo en la caché, aunque el programa nunca lea lógicamente ese dato.

La mecánica típica es indirecta: el acceso especulativo deja una huella en la caché que depende del valor secreto, y después el atacante cronometra una batería de accesos para ver cuál responde rápido. Esa posición veloz delata el byte robado.

La fuga no ocurre en la memoria del lenguaje, sino en el tiempo del hardware. Por eso la memory-safety no la detiene: el ataque no incumple ninguna regla de JavaScript, se cuela por debajo de las reglas del lenguaje, en la física del procesador.

Cuando Spectre se hizo público en 2018 sacudió a toda la industria precisamente por esto: afectaba a casi todos los procesadores modernos y no era un fallo de software que se pudiera corregir con un simple parche de código.

La lección que Cloudflare extrajo fue contraria a la sabiduría convencional. En vez de concluir que el multi-tenant en software era imposible, se preguntó qué necesita el ataque para funcionar y cómo negárselo. De esa pregunta salen todas las capas que vienen a continuación.

⚠️
Todo canal lateral necesita un reloj

La observación clave, y la que abrió la estrategia de Cloudflare, es que un ataque de temporización necesita medir el tiempo con precisión. Sin un reloj fino no puedes distinguir un acierto de caché de un fallo, y el canal lateral se queda ciego. Quitar la capacidad de medir el tiempo con precisión no arregla la CPU, pero deja al atacante sin instrumento con el que leer la señal.

Las capas de defensa de Cloudflare

Cloudflare no responde con una sola técnica, sino apilando defensas que se refuerzan entre sí. Ninguna es perfecta en solitario; juntas hacen que el ataque deje de ser rentable.

flowchart TB
L1[Memory-safety de V8 sin punteros arbitrarios] --> L2[Modelo de capacidades solo bindings]
L2 --> L3[Relojes imprecisos sin temporizadores finos]
L3 --> L4[Control de la concurrencia como reloj sintetico]
L4 --> L5[Aislamiento dinamico de proceso ante conducta sospechosa]
L5 --> L6[Parcheo instantaneo de toda la flota]
style L1 fill:#a6e3a1,color:#11111b
style L3 fill:#f9e2af,color:#11111b
style L5 fill:#fab387,color:#11111b
style L6 fill:#89b4fa,color:#11111b
⏱️

Relojes deliberadamente imprecisos

El reloj de un Worker apenas avanza durante el cómputo puro: solo se actualiza en operaciones de entrada y salida. Sin temporizador fino, medir la caché se vuelve inviable.

🧵

Concurrencia controlada

Un atacante podría fabricar un reloj con un contador girando en paralelo. Por eso el runtime controla la concurrencia que permite, cerrando ese camino a construir un temporizador sintético.

🚪

Aislamiento dinámico de proceso

Si un Worker se comporta como si intentara un ataque de temporización, el sistema lo mueve a su propio proceso del sistema operativo, devolviéndole la frontera de hardware al caso sospechoso.

🛡️

Una flota, un parche

Al operar toda la plataforma, Cloudflare despliega una mitigación o un arreglo de V8 a todos los inquilinos a la vez, en lugar de esperar a que cada cliente parchee su máquina.

El aislamiento dinámico de proceso es la pieza más elegante. En lugar de pagar el coste de un proceso por inquilino para todos —volviendo al modelo pesado que el isolate vino a eliminar—, Cloudflare mantiene a la inmensa mayoría en el modelo barato y compartido.

Y reserva la frontera cara del sistema operativo solo para los raros casos que disparan la sospecha. Se paga el coste alto únicamente donde el riesgo lo justifica: una defensa que se adapta al comportamiento en lugar de aplicarse a ciegas.

Por debajo de todo esto sigue operando la defensa en profundidad clásica: el propio proceso workerd corre dentro de un sandbox del sistema operativo que restringe las llamadas que puede hacer. Incluso un fallo en V8 se encontraría otra reja detrás.

📝
La ventaja de operar una sola plataforma

Cuando aparece una vulnerabilidad nueva en V8, Cloudflare la parchea una vez y el arreglo llega a toda la red en cuestión de horas. En una nube de máquinas virtuales gestionadas por cada cliente, ese mismo fallo se parchea tantas veces como clientes haya, y a la velocidad de cada uno. Centralizar el runtime convierte la seguridad en una operación que se hace una vez para todos.

Qué significa seguro por diseño

Mover el aislamiento al software no lo debilitó: lo volvió auditable y parcheable

La intuición heredada dice que el hardware es la única frontera de confianza seria y que renunciar a él es rebajar la seguridad. Este nivel debería haberte convencido de lo contrario. Al llevar el aislamiento al lenguaje, Cloudflare no eliminó la defensa: la transformó en algo que un proceso o una VM nunca podrán ser. Una frontera de hardware es opaca y rígida; una frontera de lenguaje es código que se puede leer, auditar, endurecer y, sobre todo, parchear de golpe para todo el planeta cuando aparece una amenaza nueva. El modelo no descansa en una única muralla que, si cae, lo pierde todo, sino en capas independientes: la memory-safety de un motor probado por miles de millones de navegadores, un modelo de capacidades que niega la autoridad ambiental, relojes cegados que desarman los ataques de temporización, control de la concurrencia que impide reconstruir esos relojes, y un aislamiento de proceso que resurge dinámicamente solo donde hace falta. Frente a Spectre, la jugada maestra fue no intentar arreglar la CPU, sino negarle al atacante los dos ingredientes que todo canal lateral necesita: un reloj preciso y la ejecución concurrente para construirlo. Seguro por diseño no significa “imposible de atacar”, una promesa que nadie honesto hace; significa que la seguridad es un sistema de capas comprensibles, cada una cubriendo el punto ciego de la anterior, todas mejorables de forma centralizada. Ese es el verdadero cierre del nivel: los isolates no son contenedores no porque sean más ligeros, sino porque encarnan una filosofía distinta de qué es una frontera de confianza y de cómo se sostiene.

⚔️ Razona la seguridad del modelo
  1. Explica por qué la memory-safety de V8 detiene a un inquilino que quiere leer el heap de otro, pero no detiene un ataque de canal lateral como Spectre.
  2. Describe, en tus palabras, por qué “todo ataque de temporización necesita un reloj” convierte la restricción de temporizadores en una defensa real.
  3. Argumenta por qué controlar la concurrencia es necesario aunque ya se hayan cegado los relojes: qué agujero cerraría un contador girando en paralelo.
  4. Explica por qué el aislamiento dinámico de proceso conserva la ventaja de densidad de los isolates en vez de destruirla.
  5. Compara el parcheo de una vulnerabilidad de V8 en la flota de Cloudflare con el parcheo del mismo fallo en una nube de VMs gestionadas por cada cliente.
  6. Defiende o critica la frase “seguro por diseño” a partir de la idea de defensa en capas: qué promete y qué no promete.