Volver a todas las publicaciones
Publicado el · por Renaud Deraison

El service worker que no quiere morir, y la VM a la que no le importa

Google republicó accidentalmente la semana pasada un bug de Chromium de hace cuatro años — un service worker que sigue ejecutando JavaScript después de cerrar el navegador, en todos los navegadores Chromium principales, todavía sin parchar. La prueba de concepto ya está circulando. La pregunta interesante no es cómo funciona. Es qué significa la persistencia en un navegador cuya máquina subyacente entera deja de existir cuando cierras la pestaña.

La persistencia no es una propiedad del código. Es una propiedad del lugar al que se le permite vivir al código. Un service worker que «nunca muere» tiene un problema cuando la máquina en la que fue instalado se destruye cada vez que cierras la ventana.

El 20 de mayo de 2026, el rastreador de incidencias de Chromium retiró silenciosamente las restricciones de acceso sobre un bug que llevaba en estado restringido desde 2022. En cuestión de horas, el bug — junto con una prueba de concepto funcional — estaba siendo replicado, archivado y comentado por toda la web. Google intentó volver a cerrar la incidencia. Era demasiado tarde; la página ya estaba en la Wayback Machine, y el artículo de BleepingComputer salió al día siguiente.

El bug es real, la prueba de concepto funciona y, cuatro años después de su informe inicial, sigue sin estar parchado en Chrome Dev 150 ni en Edge 148. Afecta a todos los navegadores derivados de Chromium: Chrome, Edge, Brave, Opera, Vivaldi, Arc.

Lo que permite hacer a una página es, como frase, mundano: registrar un service worker — un pequeño fragmento de JavaScript que el navegador acepta mantener en ejecución en segundo plano — que no deja de ejecutarse cuando la página se cierra, cuando la pestaña se cierra, cuando el navegador se cierra, cuando la máquina se reinicia. La investigadora Lyra Rebane, que reportó originalmente el bug en diciembre de 2022, le resumió a BleepingComputer el peor caso: en Microsoft Edge el menú de descargas ni siquiera aparece, con lo que el resultado es «un RCE de JS completamente silencioso que sigue ejecutándose incluso después de que cierres el navegador».

El detalle técnico de por qué existe el bug importa menos que la clase de ataque que habilita. Es una primitiva de persistencia. Una página que visitaste una vez, posiblemente hace meses, posiblemente un anuncio que no recuerdas haber visto, tiene permiso para seguir ejecutando código en tu computadora indefinidamente. Enrolamiento en una botnet, fraude publicitario, criptojacking, participación en DDoS, exfiltración lenta de datos, recolección oportunista de credenciales cada vez que vuelves a iniciar sesión en el mismo sitio — todo eso se vuelve trivial una vez que tienes un punto de apoyo que sobrevive al cierre del navegador.

Es un buen bug, en el sentido de que es revelador. Es un mal bug, en el sentido de que se va a usar.

Qué es realmente un service worker

La plataforma web lleva, desde hace alrededor de una década, permitiendo a las páginas instalar pequeños programas de JavaScript llamados service workers. Se ejecutan en segundo plano, separados de cualquier pestaña, y existen para que las aplicaciones web puedan hacer cosas razonables — cachear recursos para que un sitio cargue cuando estás sin conexión, entregar notificaciones push, sincronizar datos cuando la red vuelve.

Un service worker se registra contra un origen (digamos, la combinación de https, el hostname example.com y el puerto por defecto). Una vez registrado, el navegador lo guarda en disco y lo vuelve a traer la próxima vez que visitas cualquier cosa de ese origen — o, en el caso de funciones de segundo plano como los mensajes push, lo despierta periódicamente por su cuenta. Desde el punto de vista del usuario, el worker es invisible. Desde el punto de vista del sistema operativo, el worker es parte del navegador, ejecutándose con los permisos que tenga el proceso del navegador.

1. La página registra el workernavigator.serviceWorker.register('/sw.js')2. El navegador lo persisteescrito en el perfil en disco,indexado por origen,acotado a una ruta URL,sobrevive a los reinicios.3. El navegador lo despiertaal revisitar, al recibir push,al sincronizar en segundo planoEL PERFIL DEL NAVEGADOR EN DISCOcookies · localStorage · IndexedDB · registros de service worker— el lugar donde la «persistencia» vive realmente —
La vida normal de un service worker. Una página lo registra, el navegador lo escribe en disco contra el origen de la página, y a partir de ahí el navegador es el responsable de volver a despertarlo — cuando el usuario revisita el origen, cuando llega un push, cuando se dispara una sincronización en segundo plano. La dirección del worker es tu disco local; la vida del worker es la vida del perfil del navegador que lo aloja.

El bug que reportó Rebane es, a grandes rasgos, que una página hostil puede abusar de una API de descargas en segundo plano para registrar un worker que el navegador después se niega a terminar. Otros bugs de service worker de Chromium de la misma familia ya se han enviado antes; este es al menos el segundo en los últimos doce meses. El patrón — la página planta algo, el navegador lo guarda, el navegador lo trae de vuelta más tarde por su cuenta — es la parte que merece la pena retener. Es también el patrón que se estrella contra un muro en el momento en que el navegador deja de ser dueño de un trozo de disco de larga duración.

Qué requiere la persistencia

La persistencia — en el sentido de seguridad, no en el sentido de plataforma web — es el movimiento que hace un atacante inmediatamente después del acceso inicial. Una página o un binario aterriza en tu máquina y, antes de hacer cualquier otra cosa interesante, se las arregla para que alguna parte de sí mismo sobreviva a los pasos de limpieza evidentes: cerrar la pestaña, salir del navegador, reiniciar, dejar el portátil suspendido en el tren de vuelta a casa. En un SO de escritorio, la persistencia tiene su propio vocabulario — launch agents en macOS, tareas programadas en Windows, unidades de usuario de systemd en Linux, extensiones del navegador, entradas de autoarranque.

Un service worker es, en efecto, un launch agent dentro del navegador. Se registra, se almacena y se trae de vuelta a la vida según un calendario que controla el navegador. Es una de las primitivas de persistencia más elegantes que ofrece la plataforma web, lo cual es parte de por qué sigue atrayendo bugs.

Crucialmente, cada forma de persistencia de esa lista tiene el mismo requisito, expresado en tres palabras: algo debe persistir. Un launch agent persiste porque hay un sistema de archivos en el que se escribe el archivo .plist del agente, y el agente se vuelve a leer desde ese sistema de archivos en cada arranque. Un service worker persiste porque hay un perfil del navegador en el que se escribe el registro del worker, y el registro se vuelve a leer cada vez que el navegador arranca. Quita la capa de almacenamiento y la «persistencia» se va con ella.

Esa frase — quita la capa de almacenamiento y la persistencia se va con ella — es la parte de la arquitectura alrededor de la cual se construyó Bromure.

Cómo ejecuta un navegador Bromure

Una sesión de Bromure no es un proceso. Es una máquina virtual. Cuando abres una ventana del navegador, el host lanza un pequeño invitado Linux desechable sobre el hipervisor de Apple Silicon. El proceso del navegador dentro de ese invitado es donde se ejecutan tus pestañas. El invitado tiene su propio sistema de archivos, su propia memoria, su propio kernel. Ninguna de esas cosas es del host.

Hay dos piezas de esa arquitectura que importan en particular para el bug del service worker.

Primero, el perfil en disco en el que normalmente viviría el registro de un service worker está dentro del sistema de archivos del invitado, no en el sistema de archivos de tu Mac. Cuando el invitado desaparece, ese sistema de archivos se va con él. Segundo, el invitado en sí tiene una vida que fija el usuario — por defecto, un invitado está ligado a un perfil (trabajo, personal, banca, un sandbox para codificar con un LLM) y se borra cuando el usuario lo elige, pero para pestañas efímeras el invitado se destruye cuando se cierra la ventana. Incluso en modo perfil, el invitado es una instantánea de volumen de disco que el usuario puede revertir en cualquier momento.

Navegador tradicionalTU USUARIO · TU DISCOLa pestaña visita una página hostilregistra un service workerEl worker se escribe en el perfil~/Library/Application Support/...en tu disco realCierras la pestaña…el worker se queda en disco…El navegador lo revive despuésal siguiente lanzamiento, con un push, con un sync —el JS sigue ejecutándose. Durante meses.BromureVM INVITADA DESECHABLESISTEMA DE ARCHIVOS DEL INVITADOLa pestaña visita una página hostilregistra un service workerEl worker se escribe en el disco invitadodentro de la VM — no en el hostnunca toca el sistema de archivos de tu MacCierras la pestañaLa VM se destruyeel disco del invitado se va con ella.nada que revivir.
El mismo service worker hostil registrado contra dos navegadores. En un navegador tradicional, el registro se escribe en un perfil del disco del host del usuario; cerrar el navegador no lo elimina, y el siguiente lanzamiento trae al worker de vuelta. En Bromure, el registro se escribe en el sistema de archivos de una VM invitada; cerrar la pestaña destruye la VM, y el registro se va con ella.

En el caso desechable, el worker que «nunca muere» no tiene desde dónde no-morir. La fila en disco desde la que se habría vuelto a leer en el siguiente arranque del navegador estaba dentro de una máquina virtual que ya no existe. El JavaScript que «siguió ejecutándose después de cerrar el navegador» solo siguió ejecutándose porque había algo sobre lo cual seguir ejecutándose. Cuando el cierre del navegador también cierra la máquina subyacente, el worker se detiene al mismo tiempo que el navegador — exactamente el comportamiento que asumía la especificación original.

En el caso del perfil — digamos, mantienes un perfil de larga duración para «redes sociales» que está pensado para recordarte entre sesiones — el worker sobrevive como siempre lo ha hecho, acotado a la VM de ese único perfil. Sigue sin poder ver tus Documentos, tu llavero, tus otros perfiles ni el resto de tu computadora. Y si alguna vez sospechas que ese perfil ha recogido algo que no debería, puedes revertir toda la VM a su última instantánea limpia, lo que tarda más o menos lo mismo que volver a lanzar un navegador.

El argumento general

Sería fácil leer lo anterior como «Bromure resulta que derrota este bug específico de service worker». Eso es cierto, pero no es la afirmación interesante. La afirmación interesante es más general, y este incidente es una pequeña pieza de evidencia a su favor.

Los días cero del navegador seguirán cayendo. Este es al menos el segundo bug de service worker de Chromium en los últimos doce meses. Los grandes de 2024 y 2025 fueron confusiones de tipo en V8 y escapes de sandbox vía Mojo, pagados a precios de seis cifras, usados por proveedores de spyware comercial y grupos alineados con estados. Hubo compromisos de renderer antes de que existieran los service workers; habrá compromisos de renderer mucho después de que este bug en particular se parche. La economía de encontrar nuevos — especialmente con auditores de IA de clase Mythos ahora en el bucle a ambos lados de la ventana de divulgación — se mueve contra el defensor, no a su favor.

La pregunta relevante, cuando se dispara un día cero, no es ¿era el navegador impecable? No lo era. La pregunta relevante es ¿a qué llegó realmente el exploit?

Muro = sandbox in-processRenderer (V8, Blink, códecs…)SANDBOX (Mojo IPC, seccomp)misma genealogía de espacio de direcciones que el bugProceso del navegador (broker)TU HOSTDocumentos · Llavero · claves SSH · ~/.awsiCloud · webcam · micrófono · red local— lo que alcanza un escape del sandbox —Muro = VM separadaRenderer (V8, Blink, códecs…)Proceso del navegador (broker) — dentro del invitadoFRONTERA DEL HIPERVISORkernel separado, espacio de direcciones separado,sin bytes compartidos con el bugTU HOSTDocumentos, Llavero, claves SSH, ~/.aws —nada de esto es visible para el invitado.— lo que alcanza un escape del sandbox: más invitado —
Dos maneras en que un atacante puede extender su alcance después de que se dispare un bug del navegador. En un navegador tradicional, el sandbox in-process es el muro entre el código del renderer y el host; cuando alguien encuentra un bug de memoria en V8 o en un parser, ese es el muro que se agrieta. En Bromure, el muro es una VM separada que no comparte espacio de direcciones con el navegador, no parsea bytes web y no se rompe cuando se rompe un renderer.

Un escape del sandbox de Chromium — el «día cero del navegador» canónico sobre el que has leído cada pocos meses durante la última década — es una cadena que toma un bug de memoria en alguna pieza del renderer (V8, Skia, WebP, Dawn, libxml2) y la usa para convencer al proceso broker del navegador de hacer algo que el renderer no debía poder hacer. Esos bugs se pagan a seis cifras porque cruzan el muro que los ingenieros de Chromium dedican la mayor parte de su tiempo a mantener: el sandbox in-process.

Ese muro es una pieza de ingeniería extraordinaria. También está, genealógicamente, hecho del mismo tipo de material que el bug que lo cruzó — C++, espacios de direcciones compartidos, primitivas IPC como Mojo. Cuando aparece un nuevo bug de memoria en V8 o en uno de los cien parsers que vienen con Chromium, es el tipo de cosa que, con suficiente trabajo, puede hacer palanca y abrir el sandbox.

Una VM es un tipo distinto de muro. El navegador que se ejecuta dentro no tiene una dirección de memoria virtual que mapee al kernel del host; el kernel del host no parsea la página que el navegador está parseando; la pila de virtualización es una superficie diminuta comparada con un navegador, y lo que parsea (estados de registro de vCPU, hypercalls paravirt, descriptores de buffer virtio) no son bytes adversariales arreglados por una página web. Hay bugs en esa frontera. Están en una liga distinta de dificultad, son varios órdenes de magnitud más raros y, cuando aparecen, los suele anunciar el fabricante del hipervisor con un CVE.

Qué cambia esto para el usuario

En el caso específico del bug del service worker expuesto la semana pasada, un usuario de Bromure no necesita esperar a un parche. El bug también existe en su navegador — Bromure envía Chromium upstream — pero el comportamiento que el bug habilita es el comportamiento que la arquitectura de Bromure ya deshabilita. Un worker que «vive para siempre» vive solo mientras viva la VM invitada en la que se registró, que es como mucho mientras dure el perfil que mantengas, y como mínimo mientras la pestaña esté abierta.

En el caso más general de «un día cero del navegador caerá en algún momento del próximo mes», un usuario de Bromure obtiene una forma de resultado distinta. El exploit se dispara, en el peor caso, contra el contenido del invitado: la sesión del navegador, las cookies de los sitios en los que hay sesión iniciada dentro de ese perfil, cualquier cosa que el usuario haya tecleado en ese navegador. Eso es una pérdida real y no intentamos ocultarla. Lo que no se pierde es el host: ni claves SSH, ni ~/.aws, ni llavero, ni Documentos, ni árbol de código fuente, ni espacio de trabajo de agente LLM.

Ese intercambio — la sesión del navegador puede verse comprometida, el host no, y la propia sesión es corta — es el intercambio que sigue teniendo sentido a medida que el ritmo de nuevos bugs del navegador sube.

Lo que no estamos afirmando

Dos matices, dichos abiertamente:

Las VMs desechables no parchan Chromium

El bug del service worker es un bug de Chromium. Las personas adecuadas para arreglarlo son los ingenieros de Chromium, y deberían hacerlo. La arquitectura de Bromure cambia el coste de que el bug aún no esté arreglado; no es un sustituto de arreglarlo. Reportamos upstream cuando encontramos cosas, y leemos los mismos avisos que todos los demás.

Un escape de VM es una categoría real

Un bug en el propio hipervisor — el framework de Virtualization de Apple, o una de las superficies más pequeñas a su alrededor — permitiría, en principio, que un atacante alcance el host. Esos bugs existen. También son varios órdenes de magnitud más raros que los bugs de memoria del navegador, y la superficie es drásticamente menor. Bromure hace que la exposición del host a un compromiso del renderer dependa de esa clase de bug, en lugar de la clase que se envía cada pocas semanas.

El titular que leerás el mes que viene

Habrá otro bug de service worker de Chromium. Habrá otra confusión de tipo en V8. Habrá otro desbordamiento de heap en libwebp. Habrá otro día cero que un adversario extremadamente bien dotado de recursos ha estado usando contra gente extremadamente específica durante un tiempo extremadamente largo, que se vuelve público un martes por la mañana con un aviso de parche de emergencia.

La pregunta útil, en la mañana de cualquiera de esos, no es si ya has reiniciado tu navegador. La pregunta útil es qué había realmente dentro de la caja en la que aterrizó el bug. Para la mayoría de los usuarios, hoy, esa caja es la misma caja que contiene sus archivos y su llavero y sus inicios de sesión guardados. Para un usuario de Bromure, la caja es una máquina virtual invitada cuya peor pérdida posible es la ventana que de todas formas estaba a punto de cerrar.

La persistencia requiere algo sobre lo que persistir. Haz que ese algo sea desechable, y toda una clase de ataque deja de ser una clase de ataque.