Comment Caputchin met les jeux en bac à sable
Un jeu Caputchin est du code tiers qui tourne dans le navigateur de ton visiteur, sur ta page. Qu'il vienne du marketplace ou que tu l'héberges toi-même, Caputchin le traite comme non fiable par défaut et l'enveloppe dans plusieurs couches d'isolation indépendantes, pour qu'un jeu boguant ou hostile puisse se rendre, se jouer et rapporter un résultat, mais ne puisse atteindre rien d'autre : ni ta page, ni les données de ton visiteur, ni le réseau.
Cette page explique chaque mesure, pourquoi chacune existe, et comment l'ensemble interagit avec une CSP que tu ajoutes à ton propre site.
Le modèle de menace en une ligne
Le jeu exécute du JavaScript arbitraire (et possiblement du WebAssembly) fourni par un tiers. L'objectif d'isolation est donc : le jeu peut calculer et dessiner, et il peut remettre un résultat au widget, mais il ne peut ni lire ni affecter quoi que ce soit hors de son propre cadre. Chaque couche ci-dessous sert ce seul objectif, et elles sont de la défense en profondeur, aucune couche unique n'est supposée parfaite.
Couche 1 : un iframe en bac à sable à origine opaque
Chaque jeu tourne dans un <iframe> que le widget construit avec un attribut sandbox délibérément minimal. Le seul jeton accordé est allow-scripts, parce que le jeu est du JavaScript et doit tourner. Tout le reste est retenu, et l'omission la plus importante est allow-same-origin.
Sans allow-same-origin, le navigateur donne au cadre une origine null unique. Ce seul fait est la frontière porteuse :
- Le jeu ne peut pas lire le DOM de ta page, les cookies, le
localStorage, ni la session Caputchin, il est scellé dans une origine jetable étrangère à la tienne. - Parce que le cadre ne peut aussi jamais naviguer ta fenêtre supérieure, ouvrir des popups, soumettre des formulaires, ni faire surgir des dialogues natifs (toutes ces capacités sont des jetons de bac à sable que Caputchin n'accorde pas), il n'y a aucun chemin du jeu vers ta page.
La combinaison d'allow-scripts sans allow-same-origin est tout l'enjeu : le navigateur exécute le code du jeu mais traite le cadre comme une origine étrangère qui ne peut toucher rien de ce qui est à toi. Caputchin n'ajoute jamais allow-same-origin à un cadre de jeu. (En conséquence, chaque message que le jeu poste au widget arrive de l'origine "null", ce que les vérifications du canal du widget attendent, une origine non-null signalerait elle-même un bac à sable mal configuré.)
Couche 2 : le document du cadre est construit en ligne, pas récupéré
Le widget ne pointe pas l'iframe vers une page distante. Il construit le document HTML entier du cadre en ligne (via srcdoc) et le remet au cadre à origine null. Ce document est minuscule : une balise meta CSP stricte (couche suivante), un petit bootstrap de runtime Caputchin, et une balise <script> qui charge le bundle du jeu.
C'est le même mécanisme pour les deux chemins de livraison, seule l'URL du bundle diffère :
| Source du jeu | URL du bundle dans le document en ligne |
|---|---|
| Marketplace (résolu par la plateforme) | L'URL du bundle épinglée et hachée pour l'intégrité que Caputchin a résolue au montage. |
Auto-hébergé (ton game-src) | L'URL que tu as fournie. |
Parce que le document est rédigé par le widget plutôt que par le jeu, le jeu ne contrôle jamais la CSP, le runtime, ni la structure du cadre, seulement ce que son propre bundle fait une fois chargé.
Couche 3 : Subresource Integrity pour les jeux du marketplace
Quand Caputchin sert un jeu du marketplace, il épingle le bundle à une version immuable et enregistre un hash cryptographique (SHA-384) de celui-ci. La balise <script> qui charge le bundle porte ce hash comme attribut integrity avec crossorigin="anonymous", donc le navigateur lui-même refuse d'exécuter le bundle si un seul octet diffère de ce qui a été épinglé. Un CDN compromis ne peut pas substituer un code différent ; le chargement échoue en mode fermé.
Un jeu auto-hébergé n'a aucun hash affirmé par la plateforme (tu es la racine de confiance de tes propres octets), donc l'attribut integrity est simplement omis pour ce chemin. Vois le contrat de rejeu pour comment le résultat d'un jeu du marketplace est re-dérivé indépendamment sur le serveur, une garantie distincte de l'intégrité.
Couche 4 : une stricte Content-Security-Policy en ligne
Le document en ligne porte une Content-Security-Policy serrée. Même si le cadre est déjà à origine null et en bac à sable, la CSP contraint en plus ce que le code chargé peut faire, elle nie tout par défaut et ne re-accorde que le minimum :
| Directive | Valeur | Pourquoi |
|---|---|---|
default-src | 'none' | Nier tout ce qui n'est pas explicitement autorisé ci-dessous. |
script-src | un hash du propre runtime en ligne de Caputchin + l'origine du bundle du jeu + 'wasm-unsafe-eval' | N'exécuter que le bootstrap exact de Caputchin (épinglé par un hash sha256-, pas 'unsafe-inline') et le propre bundle du jeu. 'wasm-unsafe-eval' laisse un moteur WASM compiler sans accorder le plein 'unsafe-eval'. |
connect-src | 'none' | La directive anti-exfiltration. Le jeu ne peut pas fetch, faire de XHR, ouvrir un WebSocket, ni envoyer un beacon où que ce soit. Aucune sortie réseau du tout. |
img-src | data: (plus toute origine de ressource de skin, ci-dessous) | Sprites en ligne, ou depuis un hôte de ressource autorisé. |
media-src | data: (plus toute origine de ressource de skin, ci-dessous) | Audio/vidéo en ligne, ou depuis un hôte de ressource autorisé. |
font-src | data: | Polices en ligne seulement. |
style-src | 'unsafe-inline' | Les jeux fixent des styles en ligne ; pas de feuilles de style externes. (Les styles ne peuvent pas exfiltrer des données comme le script ou le réseau le peuvent.) |
L'effet net : le jeu tourne, se rend et rapporte son résultat via le pont du SDK, et ne peut atteindre rien d'autre, surtout pas le réseau. Cette CSP est fixée par Caputchin et n'est pas quelque chose que tu configures ; elle est listée ici pour que tu comprennes exactement à quel point un jeu est confiné.
Le seul endroit où un réglage client élargit la CSP du cadre
Les skins peuvent pointer des champs image ou audio vers une URL absolue sur ton propre CDN (vois skins). Pour que ces ressources se chargent dans le cadre verrouillé, Caputchin ajoute seulement ces origines de ressource exactes au img-src / media-src du cadre, et nulle part ailleurs. script-src et connect-src ne sont jamais élargis, donc même une origine de ressource autorisée ne peut pas servir à charger du code ni à ouvrir un canal réseau. C'est l'unique façon étroite dont une valeur configurée affecte la politique du cadre, et c'est encore seulement pour les ressources.
Couche 5 : un canal de résultat à sens unique
Le jeu n'a aucune surface appelable dans ta page. La seule sortie est le pont du SDK : le jeu appelle une seule méthode qui postMessage son résultat opaque (sa trace) au widget, qui vit sur ton origine. Le widget est ce qui parle à l'API de Caputchin ; le jeu ne le fait jamais (il ne peut pas, connect-src est 'none'). Donc le chemin de confiance est jeu → widget → ton backend, et chaque saut ne fait jamais que passer un résultat en avant, sans jamais accorder au jeu une portée en arrière.
La CSP que tu fixes sur ta propre page
Les couches ci-dessus te protègent toi et ton visiteur du jeu. Une Content-Security-Policy sur ta propre page est un contrôle différent et complémentaire : elle protège ta page de tout, et Caputchin est conçu pour tourner sous une stricte. Tu n'as pas à relâcher ta CSP pour intégrer Caputchin ; tu dois autoriser les quelques origines que le widget utilise légitimement.
Au minimum, le widget a besoin de :
| Directive | Autoriser | Pourquoi |
|---|---|---|
script-src | l'origine d'où tu charges le script du widget, plus 'unsafe-eval' | Le <script> qui définit <caputchin-widget> / <caputchin-game>, le CDN que tu as choisi (jsDelivr, ton propre hôte, ou caputchin.com). 'unsafe-eval' laisse tourner l'épreuve d'instrumentation ; il est requis tant que l'instrumentation est active, et tu peux le retirer en désactivant l'instrumentation sur la page Sécurité de la clé. |
connect-src | https://caputchin.com | Le widget appelle l'API Caputchin pour mettre en place et confirmer une vérification. Si tu pointes le widget vers un autre hôte d'API, autorise celui-là à la place. |
frame-src | https://caputchin.com | Gouverne l'iframe du jeu. Parce que le cadre utilise un document srcdoc en ligne, les navigateurs lui appliquent ton frame-src, donc autorise l'origine Caputchin ici. |
worker-src | blob: | Le solveur de proof of work tourne entièrement dans des Web Workers créés à partir d'une URL blob:, sans repli sur le fil principal, donc c'est requis. Si tu omets worker-src, les navigateurs retombent sur child-src puis default-src, et default-src 'self' seul ne permet pas blob:. |
Optionnellement, ajoute 'wasm-unsafe-eval' à script-src : il laisse le solveur de proof of work tourner en WebAssembly rapide. Ce n'est pas requis, le solveur retombe sur une implémentation JavaScript pure plus lente (toujours dans le Worker) sans lui.
Tu n'as pas besoin d'autoriser l'origine du propre bundle du jeu (jsDelivr, le CDN d'un jeu, ton hôte game-src) dans la CSP de ta page : ce bundle se charge à l'intérieur du cadre en bac à sable, sous la propre CSP du cadre décrite ci-dessus, pas sous la politique de ta page. Ta page ne fait qu'encadrer Caputchin ; Caputchin confine ce qui tourne à l'intérieur.
Si un élément Caputchin échoue silencieusement à se charger sous ta CSP, la console de ton navigateur nomme la directive bloquée ; ajoute l'origine ou le mot-clé qu'elle nomme à cette directive. Commence strict et ouvre exactement ce que la console demande. Tu n'as jamais besoin de 'unsafe-inline'. Tu as besoin de blob: pour les workers (worker-src) et, tant que l'instrumentation est active, de 'unsafe-eval' dans script-src ; l'exigence 'unsafe-eval' disparaît si tu désactives l'instrumentation sur la page Sécurité de la clé. 'wasm-unsafe-eval' est optionnel (il accélère le solveur de proof of work, qui sinon tourne plus lentement en JavaScript dans le même Worker).
Pourquoi tant de couches
Chaque couche serait une frontière significative à elle seule ; ensemble, elles font qu'une défaillance dans l'une n'est pas une brèche. L'origine null seule mure déjà le jeu loin de tes données ; la CSP seule tue déjà la sortie réseau ; l'intégrité seule épingle déjà les octets exacts. Caputchin les exécute toutes parce que le code tiers non fiable est exactement l'endroit où la défense en profondeur gagne son salaire.
Voir aussi
- Comment les jeux résistent aux bots : le côté qui fait autorité côté serveur de l'intégrité du jeu.
- Le contrat de rejeu : comment le résultat d'un jeu est re-dérivé indépendamment côté serveur.
- Bâtir un jeu auto-hébergé : les contraintes de bundle que ces bacs à sable imposent aux auteurs.
- Charger le widget depuis un CDN : choisir l'origine du script que la CSP de ta page doit autoriser.