Cómo Caputchin pone los juegos en sandbox
Un juego de Caputchin es código de terceros que corre en el navegador de tu visitante, en tu página. Venga del marketplace o lo alojes tú mismo, Caputchin lo trata como no confiable por defecto y lo envuelve en varias capas de aislamiento independientes, así que un juego con bugs u hostil puede renderizar, jugar y reportar un resultado, pero no puede alcanzar nada más: ni tu página, ni los datos de tu visitante, ni la red.
Esta página explica cada medida, por qué existe cada una, y cómo todo el conjunto interactúa con una CSP que añades a tu propio sitio.
El modelo de amenazas en una línea
El juego corre JavaScript arbitrario (y posiblemente WebAssembly) suministrado por un tercero. El objetivo de aislamiento es por tanto: el juego puede computar y dibujar, y puede entregar un resultado de vuelta al widget, pero no puede leer ni afectar nada fuera de su propio marco. Cada capa de abajo sirve a ese único objetivo, y son defensa en profundidad, ninguna capa por sí sola se confía como perfecta.
Capa 1: un iframe en sandbox de origen opaco
Cada juego corre dentro de un <iframe> que el widget construye con un atributo sandbox deliberadamente mínimo. El único token concedido es allow-scripts, porque el juego es JavaScript y debe correr. Todo lo demás se retiene, y la omisión más importante es allow-same-origin.
Sin allow-same-origin el navegador le da al marco un origen nulo único. Ese único hecho es el límite que carga el peso:
- El juego no puede leer el DOM, las cookies, el
localStoragede tu página, ni la sesión de Caputchin, está sellado en un origen desechable que es ajeno al tuyo. - Como el marco tampoco puede nunca navegar tu ventana superior, abrir popups, enviar formularios, ni hacer aparecer diálogos nativos (todas esas capacidades son tokens de sandbox que Caputchin no concede), no hay camino del juego de vuelta a tu página.
La combinación de allow-scripts sin allow-same-origin es el punto entero: el navegador ejecuta el código del juego pero trata el marco como un origen ajeno que no puede tocar nada tuyo. Caputchin nunca añade allow-same-origin a un marco de juego. (Como consecuencia, cada mensaje que el juego postea al widget llega del origen "null", que las comprobaciones del canal del widget esperan, un origen no nulo señalaría por sí mismo un sandbox mal configurado.)
Capa 2: el documento del marco se construye inline, no se descarga
El widget no apunta el iframe a una página remota. Construye el documento HTML entero del marco inline (vía srcdoc) y se lo entrega al marco de origen nulo. Ese documento es diminuto: una meta tag de CSP estricta (siguiente capa), un pequeño bootstrap del runtime de Caputchin, y una tag <script> que carga el bundle del juego.
Este es el mismo mecanismo para ambos caminos de entrega, solo difiere la URL del bundle:
| Fuente del juego | URL del bundle en el documento inline |
|---|---|
| Marketplace (resuelto por la plataforma) | La URL del bundle, fijada y con hash de integridad, que Caputchin resolvió en el montaje. |
Autoalojado (tu game-src) | La URL que suministraste. |
Como el documento lo crea el widget en vez del juego, el juego nunca controla la CSP, el runtime, ni la estructura del marco, solo lo que su propio bundle hace una vez cargado.
Capa 3: Subresource Integrity para juegos del marketplace
Cuando Caputchin sirve un juego del marketplace fija el bundle a una versión inmutable y registra un hash criptográfico (SHA-384) de él. La tag <script> que carga el bundle lleva ese hash como un atributo integrity con crossorigin="anonymous", así que el propio navegador se niega a ejecutar el bundle si un solo byte difiere de lo que se fijó. Un CDN comprometido no puede sustituir código distinto; la carga falla cerrada.
Un juego autoalojado no tiene hash afirmado por la plataforma (tú eres la raíz de confianza de tus propios bytes), así que el atributo integrity simplemente se omite para ese camino. Mira el contrato de repetición para cómo el resultado de un juego del marketplace se vuelve a derivar de forma independiente en el servidor, una garantía separada de la integridad.
Capa 4: una Content-Security-Policy inline estricta
El documento inline lleva una Content-Security-Policy apretada. Aunque el marco ya es de origen nulo y está en sandbox, la CSP constriñe aún más lo que el código cargado puede hacer, niega todo por defecto y vuelve a conceder solo el mínimo:
| Directiva | Valor | Por qué |
|---|---|---|
default-src | 'none' | Niega todo lo no permitido explícitamente abajo. |
script-src | un hash del propio runtime inline de Caputchin + el origen del bundle del juego + 'wasm-unsafe-eval' | Corre solo el bootstrap exacto de Caputchin (fijado por un hash sha256-, no 'unsafe-inline') y el propio bundle del juego. 'wasm-unsafe-eval' deja que un motor WASM compile sin conceder el 'unsafe-eval' completo. |
connect-src | 'none' | La directiva anti-exfiltración. El juego no puede fetch, XHR, abrir un WebSocket, ni hacer beacon a ningún sitio. Sin salida de red en absoluto. |
img-src | data: (más cualquier origen de asset de skin, abajo) | Sprites inline, o de un host de asset permitido. |
media-src | data: (más cualquier origen de asset de skin, abajo) | Audio/vídeo inline, o de un host de asset permitido. |
font-src | data: | Solo fuentes inline. |
style-src | 'unsafe-inline' | Los juegos fijan estilos inline; sin hojas de estilo externas. (Los estilos no pueden exfiltrar datos como sí pueden el script o la red.) |
El efecto neto: el juego corre, renderiza, y reporta su resultado a través del puente del SDK, y no puede alcanzar nada más, especialmente no la red. Esta CSP la fija Caputchin y no es algo que tú configures; está listada aquí para que entiendas exactamente cuán confinado está un juego.
El único sitio donde un ajuste del cliente ensancha la CSP del marco
Los skins pueden apuntar campos de imagen o audio a una URL absoluta en tu propio CDN (mira skins). Para que esos assets carguen dentro del marco cerrado, Caputchin añade solo esos orígenes de asset exactos al img-src / media-src del marco, y en ningún otro sitio. script-src y connect-src nunca se ensanchan, así que ni siquiera un origen de asset permitido puede usarse para cargar código o abrir un canal de red. Esta es la única y estrecha forma en que un valor configurado afecta a la política del marco, y sigue siendo solo de assets.
Capa 5: un canal de resultado de una sola dirección
El juego no tiene superficie llamable hacia tu página. La única salida es el puente del SDK: el juego llama a un único método que hace postMessage de su resultado opaco (su traza) al widget, que vive en tu origen. El widget es el que habla con la API de Caputchin; el juego nunca lo hace (no puede, connect-src es 'none'). Así que el camino de confianza es juego → widget → tu backend, y cada salto solo pasa un resultado hacia adelante, nunca concede al juego alcance hacia atrás.
La CSP que fijas en tu propia página
Las capas de arriba te protegen a ti y a tu visitante del juego. Una Content-Security-Policy en tu propia página es un control distinto y complementario: protege tu página de todo.
Un hecho manda sobre todo lo de esta sección: el marco del juego hereda la política de tu página. El marco es un documento srcdoc inline, y los navegadores les aplican la CSP de la página que los incrusta encima de la propia del marco. Así que la política blindada del marco que se muestra arriba es un piso que añade Caputchin, no un reemplazo de la tuya. Todo lo que corre dentro del marco tiene que satisfacer ambas, lo que significa que la política de tu página tiene que permitir el bundle del juego y el propio bootstrap del marco, aunque tu página no cargue ninguno de los dos directamente.
Como mínimo, el widget necesita:
| Directiva | Permitir | Por qué |
|---|---|---|
script-src | el origen del que cargas el script del widget, más 'unsafe-eval' | El <script> que define <caputchin-widget> / <caputchin-game>, el CDN que elegiste (jsDelivr, tu propio host, o caputchin.com). 'unsafe-eval' deja correr el reto de instrumentación; es obligatorio mientras la instrumentación está activada, y puedes quitarlo apagando la instrumentación en la página de Seguridad de la clave. |
script-src (solo juegos) | el origen del bundle del juego, más 'unsafe-inline' y 'wasm-unsafe-eval' | Porque el marco hereda esta política. El origen del bundle es https://games.caputchin.com para los juegos del marketplace, o tu propio host / CDN para un juego autoalojado. 'unsafe-inline' cubre el propio script de bootstrap del marco, que el marco fija por hash sha256- pero tu página no puede, porque ese hash cambia con cada release del widget. 'wasm-unsafe-eval' lo necesitan los juegos construidos sobre un motor WebAssembly. Nada de esto aplica si solo usas <caputchin-widget> sin juego. |
connect-src | https://verify.caputchin.com, más el origen del que cargas el script del widget | Son dos llamadas distintas. El widget llama al servicio de verificación de Caputchin para montar y confirmar una verificación, así que permite ese origen exacto (si apuntas el widget a un host de API distinto, permite ese en su lugar). El solver de proof-of-work además descarga su módulo de WebAssembly de junto al script del widget, así que el origen del CDN va aquí tanto como en script-src. Si alojas el widget en tu propio origen, 'self' cubre el segundo. |
worker-src | blob: | El solver de proof-of-work corre enteramente en Web Workers creados desde una URL blob:, sin fallback en el hilo principal, así que esto es obligatorio. Si omites worker-src, los navegadores recaen en child-src y luego en default-src, y default-src 'self' por sí solo no permite blob:. |
Añade 'wasm-unsafe-eval' a script-src para que el solver de proof-of-work corra como WebAssembly rápido. Ni esa palabra clave ni el origen del que el solver descarga son obligatorios para que la verificación salga bien: si falta cualquiera de los dos, el solver recae en una implementación más lenta de JavaScript puro (todavía dentro del Worker) y la verificación sigue funcionando. Pero solo rinden juntos. El solver hace fetch del módulo de WebAssembly y luego lo compila, así que la vía rápida necesita el origen del script del widget en connect-src y 'wasm-unsafe-eval' en script-src; permitir uno sin el otro te deja en el solver lento sin nada en la consola que diga por qué.
No necesitas una entrada de frame-src para el juego. El marco del juego es un documento srcdoc inline en vez de un documento descargado de una URL, así que no hay URL que frame-src pueda emparejar, y el marco carga incluso bajo frame-src 'none'.
Lo que sí necesitas, y lo que sorprende a la gente, es el origen del bundle del juego en el script-src de tu página, aunque tu página nunca cargue ese bundle: lo carga el marco, y el marco corre bajo tu política además de la suya. Lo mismo vale para el bootstrap del marco y para un motor de juego en WebAssembly. Concederlos no ensancha lo que el juego puede alcanzar: la propia política del marco sigue fijando connect-src 'none', el marco sigue siendo de origen opaco, y lo que concede tu página no puede aflojar un piso que el marco se pone a sí mismo.
'unsafe-inline' en script-src es la parte incómoda de esa lista, y es honesto decirlo: hoy es obligatorio para correr un juego, debilita la protección de tu propia página contra scripts inline, y la alternativa (fijar el hash sha256- del bootstrap del marco) no es práctica mientras ese hash se mueva con cada release del widget. Si ese canje no te vale para una página concreta, usa ahí <caputchin-widget>, que no necesita nada de la fila de solo juegos.
Si un elemento de Caputchin falla silenciosamente al cargar bajo tu CSP, la consola de tu navegador nombra la directiva bloqueada; añade el origen o la palabra clave que nombra a esa directiva. Empieza estricto y abre exactamente lo que la consola pide, y lee las violaciones reportadas contra about:srcdoc con tanto cuidado como las de tu propia página: esas son el marco del juego chocando con tu política heredada, y son las que la gente pasa por alto. Sí necesitas blob: para los workers (worker-src) y, mientras la instrumentación está activada, 'unsafe-eval' en script-src; el requisito de 'unsafe-eval' desaparece si apagas la instrumentación en la página de Seguridad de la clave. 'unsafe-inline' solo lo necesitas para correr juegos, según la tabla de arriba. La vía rápida del proof-of-work es opcional pero va en pareja: 'wasm-unsafe-eval' en script-src y el origen del script del widget en connect-src. Ese par es el único caso en el que la consola no te va a ayudar, porque recaer en el solver de JavaScript no es un error.
Una política que funciona
Cargando el widget desde jsDelivr, usando juegos del marketplace, con la instrumentación activada:
default-src 'none';
script-src https://cdn.jsdelivr.net https://games.caputchin.com 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval';
connect-src https://verify.caputchin.com https://cdn.jsdelivr.net;
worker-src blob:;
img-src 'self' data: https:;
style-src 'unsafe-inline'Quita https://games.caputchin.com, 'unsafe-inline' y 'wasm-unsafe-eval' si no usas juegos. Quita 'unsafe-eval' si apagas la instrumentación. Cambia https://cdn.jsdelivr.net por tu propio origen tanto en script-src como en connect-src si alojas el widget tú mismo.
Por qué tantas capas
Cada capa sería un límite significativo por sí sola; juntas significan que un fallo en una no es una brecha. El origen nulo por sí solo ya amuralla el juego lejos de tus datos; la CSP por sí sola ya mata la salida de red; la integridad por sí sola ya fija los bytes exactos. Caputchin corre todas ellas porque el código de terceros no confiable es exactamente el lugar donde la defensa en profundidad se gana el sueldo.
Véase también
- Cómo los juegos resisten a los bots: el lado autoritativo del servidor de la integridad del juego.
- El contrato de repetición: cómo el resultado de un juego se vuelve a derivar de forma independiente en el servidor.
- Construye un juego autoalojado: las restricciones de bundle que estos sandboxes imponen a los autores.
- Carga el widget desde un CDN: elegir el origen del script que la CSP de tu página debe permitir.