Caputchin
Entendendo a Caputchin

Como a Caputchin coloca os jogos em sandbox

Um jogo Caputchin é código de terceiros que roda no navegador do seu visitante, na sua página. Quer venha do marketplace, quer você mesmo o hospede, a Caputchin o trata como não-confiável por padrão e o envolve em várias camadas de isolamento independentes, de modo que um jogo bugado ou hostil possa renderizar, jogar e reportar um resultado, mas não consiga alcançar mais nada: nem sua página, nem os dados do seu visitante, nem a rede.

Esta página explica cada medida, por que cada uma existe, e como tudo interage com uma CSP que você adiciona ao seu próprio site.

O modelo de ameaça em uma linha

O jogo roda JavaScript arbitrário (e possivelmente WebAssembly) fornecido por um terceiro. A meta de isolamento é, portanto: o jogo pode computar e desenhar, e pode entregar um resultado de volta ao widget, mas não pode ler nem afetar nada fora do seu próprio frame. Cada camada abaixo serve a essa única meta, e elas são defesa em profundidade, nenhuma camada sozinha é confiada como perfeita.

Camada 1: um iframe em sandbox de origem opaca

Todo jogo roda dentro de um <iframe> que o widget constrói com um atributo sandbox deliberadamente mínimo. O único token concedido é allow-scripts, porque o jogo é JavaScript e precisa rodar. Tudo o mais é retido, e a omissão mais importante é allow-same-origin.

Sem allow-same-origin o navegador dá ao frame uma origem null única. Esse único fato é a fronteira de sustentação:

  • O jogo não consegue ler o DOM, os cookies, o localStorage da sua página, nem a sessão Caputchin, ele está selado em uma origem descartável que é estranha à sua.
  • Como o frame também nunca consegue navegar sua janela de topo, abrir popups, enviar formulários, ou abrir diálogos nativos (todas essas capacidades são tokens de sandbox que a Caputchin não concede), não há caminho do jogo de volta para fora até sua página.

A combinação de allow-scripts sem allow-same-origin é o ponto inteiro: o navegador executa o código do jogo mas trata o frame como uma origem estranha que não pode tocar em nada seu. A Caputchin nunca adiciona allow-same-origin a um frame de jogo. (Como consequência, toda mensagem que o jogo posta ao widget chega da origem "null", que as checagens de canal do widget esperam, uma origem não-null em si sinalizaria uma sandbox mal configurada.)

Camada 2: o documento do frame é construído inline, não buscado

O widget não aponta o iframe para uma página remota. Ele constrói o documento HTML inteiro do frame inline (via srcdoc) e o entrega ao frame de origem null. Esse documento é minúsculo: uma meta tag de CSP estrita (próxima camada), um pequeno bootstrap do runtime Caputchin, e uma tag <script> que carrega o bundle do jogo.

Este é o mesmo mecanismo para os dois caminhos de entrega, só a URL do bundle difere:

Origem do jogoURL do bundle no documento inline
Marketplace (resolvido pela plataforma)A URL do bundle fixada, com hash de integridade, que a Caputchin resolveu na montagem.
Auto-hospedado (seu game-src)A URL que você forneceu.

Como o documento é criado pelo widget em vez do jogo, o jogo nunca controla a CSP, o runtime, ou a estrutura do frame, só o que seu próprio bundle faz uma vez carregado.

Camada 3: Subresource Integrity para jogos de marketplace

Quando a Caputchin serve um jogo de marketplace ela fixa o bundle em uma versão imutável e registra um hash criptográfico (SHA-384) dele. A tag <script> que carrega o bundle carrega esse hash como um atributo integrity com crossorigin="anonymous", então o próprio navegador se recusa a executar o bundle se um único byte diferir do que foi fixado. Um CDN comprometido não pode substituir código diferente; o carregamento falha de forma fechada.

Um jogo auto-hospedado não tem hash afirmado pela plataforma (você é a raiz de confiança dos seus próprios bytes), então o atributo integrity é simplesmente omitido para esse caminho. Veja o contrato de replay para como o resultado de um jogo de marketplace é rederivado de forma independente no servidor, uma garantia separada da integridade.

Camada 4: uma Content-Security-Policy inline estrita

O documento inline carrega uma Content-Security-Policy apertada. Mesmo que o frame já seja de origem null e em sandbox, a CSP restringe ainda mais o que o código carregado pode fazer, ela nega tudo por padrão e reconcede só o mínimo:

DiretivaValorPor quê
default-src'none'Nega tudo que não for explicitamente permitido abaixo.
script-srcum hash do próprio runtime inline da Caputchin + a origem do bundle do jogo + 'wasm-unsafe-eval'Roda só o bootstrap exato da Caputchin (fixado por um hash sha256-, não 'unsafe-inline') e o próprio bundle do jogo. 'wasm-unsafe-eval' deixa um motor WASM compilar sem conceder 'unsafe-eval' completo.
connect-src'none'A diretiva anti-exfiltração. O jogo não pode fetch, XHR, abrir um WebSocket, nem fazer beacon para lugar nenhum. Nenhuma saída de rede de jeito nenhum.
img-srcdata: (mais quaisquer origens de ativo de skin, abaixo)Sprites inline, ou de um host de ativo permitido.
media-srcdata: (mais quaisquer origens de ativo de skin, abaixo)Áudio/vídeo inline, ou de um host de ativo permitido.
font-srcdata:Só fontes inline.
style-src'unsafe-inline'Os jogos definem estilos inline; sem folhas de estilo externas. (Estilos não conseguem exfiltrar dados do jeito que script ou rede conseguem.)

O efeito líquido: o jogo roda, renderiza e reporta seu resultado pela ponte do SDK, e não consegue alcançar mais nada, especialmente não a rede. Esta CSP é definida pela Caputchin e não é algo que você configura; ela é listada aqui para que você entenda exatamente quão confinado um jogo está.

O único lugar onde uma configuração do cliente alarga a CSP do frame

Os skins podem apontar campos de imagem ou áudio para uma URL absoluta no seu próprio CDN (veja skins). Para que esses ativos carreguem dentro do frame trancado, a Caputchin adiciona só essas origens de ativo exatas ao img-src / media-src do frame, e em nenhum outro lugar. script-src e connect-src nunca são alargados, então mesmo uma origem de ativo permitida não pode ser usada para carregar código ou abrir um canal de rede. Esta é a única forma estreita em que um valor configurado afeta a política do frame, e ela ainda é só para ativos.

Camada 5: um canal de resultado de mão única

O jogo não tem nenhuma superfície chamável para dentro da sua página. A única saída é a ponte do SDK: o jogo chama um único método que faz postMessage do seu resultado opaco (seu trace) ao widget, que vive na sua origem. O widget é o que fala com a API da Caputchin; o jogo nunca o faz (ele não consegue, connect-src é 'none'). Então o caminho de confiança é jogo → widget → seu backend, e cada salto só passa um resultado adiante, nunca concede ao jogo alcance para trás.

A CSP que você define na sua própria página

As camadas acima protegem você e seu visitante do jogo. Uma Content-Security-Policy na sua própria página é um controle diferente e complementar: ela protege sua página de tudo, e a Caputchin é projetada para rodar sob uma estrita. Você não precisa afrouxar sua CSP para embutir a Caputchin; você precisa permitir as poucas origens que o widget legitimamente usa.

No mínimo, o widget precisa de:

DiretivaPermitirPor quê
script-srca origem da qual você carrega o script do widget, mais 'unsafe-eval'O <script> que define <caputchin-widget> / <caputchin-game>, o CDN que você escolheu (jsDelivr, seu próprio host, ou caputchin.com). 'unsafe-eval' deixa o desafio de instrumentação rodar; ele é exigido enquanto a instrumentação está ligada, e você pode largá-lo desligando a instrumentação na página de Segurança da chave.
connect-srchttps://caputchin.comO widget chama a API da Caputchin para montar e confirmar uma verificação. Se você aponta o widget para um host de API diferente, permita esse em vez disso.
frame-srchttps://caputchin.comGoverna o iframe do jogo. Como o frame usa um documento srcdoc inline, os navegadores aplicam seu frame-src a ele, então permita a origem Caputchin aqui.
worker-srcblob:O solver de proof-of-work roda inteiramente em Web Workers criados a partir de uma URL blob:, sem fallback na thread principal, então isto é exigido. Se você omite worker-src, os navegadores recaem em child-src depois default-src, e default-src 'self' sozinho não permite blob:.

Opcionalmente adicione 'wasm-unsafe-eval' ao script-src: ele deixa o solver de proof-of-work rodar como WebAssembly rápido. Não é exigido, o solver recai em uma implementação mais lenta em JavaScript puro (ainda dentro do Worker) sem ele.

Você não precisa permitir a própria origem do bundle do jogo (jsDelivr, o CDN de um jogo, seu host game-src) na CSP da sua página: esse bundle carrega dentro do frame em sandbox, sob a própria CSP do frame descrita acima, não sob a política da sua página. Sua página só emoldura a Caputchin; a Caputchin confina o que roda dentro.

Se um elemento Caputchin silenciosamente falha em carregar sob sua CSP, o console do seu navegador nomeia a diretiva bloqueada; adicione a origem ou a palavra-chave que ele nomeia a essa diretiva. Comece estrito e abra exatamente o que o console pedir. Você nunca precisa de 'unsafe-inline'. Você precisa de blob: para os workers (worker-src) e, enquanto a instrumentação está ligada, de 'unsafe-eval' em script-src; o requisito de 'unsafe-eval' some se você desligar a instrumentação na página de Segurança da chave. 'wasm-unsafe-eval' é opcional (ele acelera o solver de proof-of-work, que de outra forma roda mais devagar em JavaScript dentro do mesmo Worker).

Por que tantas camadas

Cada camada seria uma fronteira significativa por si só; juntas elas significam que uma falha em uma não é uma brecha. A origem null sozinha já isola o jogo dos seus dados; a CSP sozinha já mata a saída de rede; a integridade sozinha já fixa os bytes exatos. A Caputchin roda todas elas porque código de terceiros não-confiável é exatamente o lugar onde a defesa em profundidade vale o investimento.

Veja também

Nesta página