Aislamiento: por qué un juego no puede tocar tu equipo
Dónde corre el script de un juego, qué puede y qué no puede hacer, las barreras del portal, del launcher y del editor, la prueba con un paquete malicioso y el riesgo que queda.
Corresponde aFormato 1, API 1 Para todos
En esta página
En Green Games los juegos los hace cualquiera, y un juego trae un script: código escrito por otra persona que corre en tu equipo. Por eso el diseño parte de una regla: el script de un juego nunca corre en la página ni en la aplicación que lo muestra. Corre encerrado, y solo puede pedir cosas a través de una lista corta de funciones.
Qué es un juego, visto desde la seguridad#
Un juego es un paquete de datos (la ficha y las escenas, en JSON) y un script. No es una página web ni un programa instalable. El único código con acceso al navegador o a tu equipo es el reproductor, que es de Green Games y es el mismo para todos los juegos.
El script corre dentro de la sala: la simulación de la partida. El reproductor le envía lo que haces con los controles y recibe, cuadro a cuadro, qué se movió y qué mostrar. Nada más cruza de un lado al otro.
Qué puede y qué no puede hacer un juego#
| Un juego puede | Un juego no puede |
|---|---|
| Crear, mover y eliminar piezas de su propio mundo | Leer o escribir archivos de tu equipo |
| Mover a los jugadores de la partida y leer sus controles (dirección y tres botones) | Usar la red: no hay fetch, ni conexiones de ningún tipo |
| Mostrar textos, cronómetros, menús y marcadores a través del reproductor | Tocar la página o la ventana: no hay DOM |
| Tocar sonidos sintetizados | Leer cookies o el almacenamiento del sitio o de la aplicación |
| Guardar su propio progreso (hasta 64 KB por juego, solo datos; un juego de mundo abierto, además, hasta 320 KB de cambios de su terreno) y sus puntajes | Leer el progreso de otro juego |
| Usar el reloj y el azar de la partida | Usar los temporizadores del navegador, crear hilos o ejecutar código armado como texto (eval) |
| Llamar a las funciones internas del launcher o del editor | |
| Leer el teclado completo, la posición de la flecha del ratón o lo que escribes |
Lo que el juego muestra también pasa por un filtro: el reproductor escribe los textos como texto (nunca como HTML), limita cantidades y tamaños, y vuelve a validar lo que guarda.
Las barreras del motor#
Son cuatro, de afuera hacia adentro. Son las que protegen una partida en el portal.
- El validador. Antes de cargar, rechaza el paquete si el script nombra red, página, almacenamiento, temporizadores, hilos o
eval. No es una barrera de seguridad, porque se puede burlar armando los nombres por partes: sirve para que un autor honesto se entere antes. Las que siguen sí lo son. - Un marco aislado. El reproductor crea un
iframeconsandbox, cuyo origen es opaco: no comparte cookies ni almacenamiento con el sitio. Ese marco lleva una política de contenido que empieza pordefault-src 'none': el navegador bloquea toda conexión de red, y tambiéneval. - Un Web Worker dentro de ese marco. Un worker es un hilo sin página: ahí no existe el DOM. Hereda la política del marco. El reproductor habla con él por un canal de mensajes directo.
- Un entorno recortado. Antes de ejecutar el script, la sala borra del entorno
fetch,XMLHttpRequest,WebSocket,importScripts,indexedDB,caches,Worker,postMessage,setTimeout,WebAssembly,navigatory decenas más.Math.randompasa a salir de la semilla de la partida yconsoleescribe en el registro del juego.
Después de eso, en el entorno del script quedan JavaScript estándar y el objeto greengames con los servicios de la API.
Contra el abuso de tiempo#
Un script también puede hacer daño sin salir de su encierro: gastando procesador. La sala mide los milisegundos de script de cada cuadro y detiene la partida si se pasan los límites (4 ms de promedio, 100 ms en un cuadro). Si el script entra en un bucle sin fin, la sala deja de enviar cuadros y el reproductor la cierra a los 3 segundos. Los números: Límites.
Las barreras del launcher#
En el launcher hay más en juego que en una página web: la aplicación puede leer y escribir en tu disco. Un juego nunca debe poder llamar a esas funciones. Hay cinco barreras.
- El script corre en un Web Worker. Es la sala del motor. Ahí no existen los canales con que la interfaz habla con la parte nativa de la aplicación, ni DOM; y el motor borra
fetch, almacenamiento, temporizadores y demás antes de ejecutarlo. - El reproductor vive en un marco aparte, con otro origen. Se carga desde un servidor local de la propia aplicación (
http://127.0.0.1, en un puerto al azar y con una ruta secreta), dentro de uniframeconsandbox. Su origen es opaco y distinto del de la interfaz del launcher: no ve su contenido, su almacenamiento ni su ficha de sesión, no puede abrir ventanas ni llevar la ventana principal a otra dirección. - Una política de contenido sin red. La página del reproductor se sirve con una política que prohíbe todo por defecto: ninguna conexión, ningún marco hijo, scripts solo de esa carpeta secreta del servidor local, nada escrito en línea, nada de
eval. No hay ningún destino de red permitido. El worker hereda esa política. - Un sello. El primer script de la página del reproductor cierra, en ese marco, los canales nativos de la WebView. El reproductor no los necesita: habla con la interfaz solo por mensajes, y la interfaz valida cada mensaje.
- Una ficha de sesión. Toda función nativa del launcher exige un secreto de 256 bits que la aplicación entrega una sola vez a su página principal. El marco del juego nunca lo recibe; cualquier llamada sin él se rechaza antes de hacer nada.
Además, antes de instalar un .ggx que importas, el launcher lo valida dos veces (con el validador del motor y otra vez en su parte nativa, sin ejecutar nada) y, cada vez que abre un juego instalado, comprueba que el archivo no cambió (por su huella SHA-256).
Las barreras del editor#
En el editor, Jugar usa el mismo reproductor del motor: mete la sala en un marco aislado de origen opaco, con una política de contenido sin red, y dentro en un Web Worker con el entorno recortado. Son las barreras 2, 3 y 4 del motor. El script que escribes, o el que generan las reglas, no corre en la página del editor.
El validador del editor te avisa mientras escribes si el script nombra algo que no existe en la sala:
fetch como error: los juegos no tienen red. Con errores, el editor no deja exportar.La prueba con un paquete malicioso#
Las barreras del launcher se comprueban con un ataque de verdad, que corre contra la aplicación real en Linux, en Windows y en macOS.
Primera parte: un juego malicioso. Es un paquete que pasa el validador, porque arma los nombres prohibidos por partes. Su script intenta 45 cosas:
- alcanzar los objetos con que la interfaz habla con la parte nativa, y la ventana que lo contiene;
- usar la red por
fetch,XMLHttpRequest,WebSocket,importScriptsy por caminos indirectos; - ejecutar código armado como texto (
evalyFunction); - usar almacenamiento y archivos;
- crear hilos y temporizadores;
- abusar del servicio
progresocon una clave con forma de ruta de archivo y un valor de 300 KB.
Segunda parte: una página atacante. Supone algo peor: que el atacante ya logró ejecutar código en la página del reproductor, no solo en el worker. Servida igual que el reproductor, esa página intenta alcanzar los objetos de la aplicación, la ventana madre y la ficha de sesión; hace 27 llamadas crudas por el canal nativo (una por cada función del launcher, sin ficha y con una ficha inventada); prueba almacenamiento; intenta llegar a un servidor de otro origen por nueve caminos (un servidor que cuenta lo que le llega); prueba eval, marcos hijos, deshacer el sello y llevar el propio marco a otra dirección.
La parte nativa cuenta los pedidos de red que salieron y las llamadas rechazadas, y se compara el estado del launcher antes y después.
En cada sistema el launcher usa la WebView que trae ese sistema, y la prueba corre sobre ella.
| Medida | Linux | Windows | macOS |
|---|---|---|---|
| Intentos logrados | 0 | 0 | 0 |
| Pedidos de red que salieron | 0 | 0 | 0 |
| Estado del launcher después | Sin cambios | Sin cambios | Sin cambios |
| De las 27 llamadas crudas sin sello, cuántas llegan a la parte nativa | 27 | 0 | 0 |
| De las que llegan, cuántas se rechazan | 27 | No llega ninguna | No llega ninguna |
Estos son los resultados del launcher 0.4.0. En macOS la prueba corre en un equipo con Apple Silicon, con la versión nativa y con la de Intel, que ahí corre traducida por el sistema. No se probó en un equipo con procesador Intel.
En los tres sistemas el canal nativo de la WebView se ve desde el marco del juego: por eso existe el sello. En Windows y en macOS ninguna llamada hecha desde ese marco llega a la parte nativa. En Linux llegan, y las rechaza la ficha de sesión: es el riesgo que se explica abajo.
El riesgo que queda#
Queda uno, solo en Linux, y conviene decirlo completo.
En la WebView de Linux, el canal nativo es visible desde cualquier marco, y el marco de trabajo con que está hecho el launcher comprueba solo el origen de la página principal. Por eso, en la prueba sin sello, las 27 llamadas llegan a la parte nativa. Las funciones del launcher las rechazan todas, por la ficha de sesión.
Pero hay funciones internas de ese marco de trabajo que no llevan ficha: mover o cerrar la ventana, el portapapeles, abrir una dirección en el navegador. Para alcanzarlas, alguien tendría que lograr dos cosas a la vez:
- ejecutar código propio en la página del reproductor, no en el worker. Haría falta una falla del motor al dibujar los datos que llegan de la sala;
- y además deshacer el sello.
La prueba no encontró forma de hacerlo. Cerrarlo del todo requiere que ese marco de trabajo compruebe en Linux el origen del marco que envía el mensaje, como ya hace en Windows y en macOS.
Dos límites más, para no prometer de más:
greengames probarno aísla. En la herramienta de línea de órdenes, el script corre en un contexto vacío de Node. Reproduce el entorno de la sala, pero no es una barrera de seguridad: no pruebes ahí juegos de desconocidos.- Los binarios no están firmados. El aislamiento protege de los juegos, no de una copia alterada del launcher. Descarga solo desde el sitio oficial y comprueba la suma: Descargar e instalar.
Qué significa si importas un .ggx de otra persona#
- Puedes abrirlo. El juego corre con todas las barreras de arriba: no lee tus archivos, no usa la red y no ve el resto del launcher.
- Si el archivo está dañado o es inválido, el launcher lo rechaza y te dice por qué, antes de guardar nada.
- Lo peor que un juego puede hacer dentro de su encierro es ser un mal juego: gastar procesador hasta que el reproductor lo corte, mostrar textos molestos o llenar su propio espacio de guardado (en el launcher, hasta 384 KB por juego: 64 KB de progreso y 320 KB de cambios del terreno de cubos).
- Lo que un juego guarda es solo suyo: su progreso y sus puntajes, en tu equipo.
- Sin telemetría. El launcher no envía datos de uso, y sin un catálogo en línea configurado no hace ninguna consulta de red.
Cómo se importa: Importar un .ggx.
Sigue con#
- El formato .ggx: qué hay dentro del archivo que importas.
- Scripts: el mismo encierro, visto por quien escribe el juego.
- Determinismo: por qué la sala usa su propio azar y su propio reloj.