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.
Un seul fait commande toute cette section : le cadre du jeu hérite de la politique de ta page. Le cadre est un document srcdoc en ligne, et les navigateurs appliquent à ces documents la CSP de la page qui les intègre, par-dessus celle du cadre lui-même. La politique verrouillée du cadre montrée plus haut est donc un plancher que Caputchin ajoute, pas un remplacement de la tienne. Tout ce qui tourne dans le cadre doit satisfaire les deux, ce qui veut dire que la politique de ta page doit permettre le bundle du jeu et le bootstrap du cadre lui-même, alors même que ta page ne charge directement ni l'un ni l'autre.
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 le défi 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é. |
script-src (jeux seulement) | l'origine du bundle du jeu, plus 'unsafe-inline' et 'wasm-unsafe-eval' | Parce que le cadre hérite de cette politique. L'origine du bundle est https://games.caputchin.com pour les jeux du marketplace, ou ton propre hôte / CDN pour un jeu auto-hébergé. 'unsafe-inline' couvre le script de bootstrap du cadre lui-même, que le cadre épingle par empreinte sha256- mais que ta page ne peut pas épingler, parce que cette empreinte change à chaque release du widget. 'wasm-unsafe-eval' est nécessaire aux jeux bâtis sur un moteur WebAssembly. Rien de tout cela ne s'applique si tu n'utilises que <caputchin-widget> sans jeu. |
connect-src | https://verify.caputchin.com, plus l'origine d'où tu charges le script du widget | Ce sont deux appels distincts. Le widget appelle le service de vérification Caputchin pour mettre en place et confirmer une vérification, donc autorise exactement cette origine (si tu pointes le widget vers un autre hôte d'API, autorise celui-là à la place). Le solveur de proof of work va en plus chercher son module WebAssembly à côté du script du widget, donc l'origine du CDN a sa place ici autant que dans script-src. Héberger le widget sur ta propre origine couvre la seconde avec 'self'. |
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:. |
Ajoute 'wasm-unsafe-eval' à script-src pour laisser le solveur de proof of work tourner en WebAssembly rapide. Ni ce mot-clé ni l'origine que le solveur interroge ne sont requis pour que la vérification aboutisse : si l'un des deux manque, le solveur retombe sur une implémentation JavaScript pure plus lente (toujours dans le Worker) et la vérification fonctionne toujours. Mais ils ne paient qu'ensemble. Le solveur fait un fetch du module WebAssembly, puis le compile, donc la voie rapide a besoin de l'origine du script du widget dans connect-src et de 'wasm-unsafe-eval' dans script-src ; autoriser l'un sans l'autre te laisse sur le solveur lent, sans rien dans la console pour dire pourquoi.
Tu n'as pas besoin d'une entrée frame-src pour le jeu. Le cadre du jeu est un document srcdoc en ligne plutôt qu'un document récupéré depuis une URL, donc il n'y a aucune URL que frame-src puisse faire correspondre, et le cadre se charge même sous frame-src 'none'.
Ce dont tu as bien besoin, et ce qui surprend, c'est l'origine du bundle du jeu dans le script-src de ta page, alors même que ta page ne charge jamais ce bundle : c'est le cadre qui le charge, et le cadre tourne sous ta politique en plus de la sienne. Idem pour le bootstrap du cadre et pour un moteur de jeu WebAssembly. Les accorder n'élargit pas ce que le jeu peut atteindre : la politique propre du cadre épingle toujours connect-src 'none', le cadre reste d'origine opaque, et ce que ta page accorde ne peut pas relâcher un plancher que le cadre s'impose à lui-même.
'unsafe-inline' dans script-src est la part inconfortable de cette liste, et il est honnête de le dire : il est requis aujourd'hui pour faire tourner un jeu, il affaiblit la protection de ta propre page contre les scripts en ligne, et l'alternative (épingler l'empreinte sha256- du bootstrap du cadre) n'est pas praticable tant que cette empreinte bouge à chaque release du widget. Si ce compromis n'est pas acceptable pour une page donnée, utilise <caputchin-widget> là-bas : il n'a besoin de rien de la ligne réservée aux jeux.
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, et lis les violations signalées contre about:srcdoc avec autant d'attention que celles contre ta propre page : ce sont le cadre du jeu qui heurte ta politique héritée, et ce sont celles que l'on rate. 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é. Tu n'as besoin de 'unsafe-inline' que pour faire tourner des jeux, selon le tableau ci-dessus. La voie rapide du proof of work est optionnelle mais va par paire : 'wasm-unsafe-eval' dans script-src et l'origine du script du widget dans connect-src. Cette paire est le seul cas où la console ne t'aidera pas, parce que retomber sur le solveur JavaScript n'est pas une erreur.
Une politique qui marche
En chargeant le widget depuis jsDelivr, avec des jeux du marketplace et l'instrumentation active :
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'Retire https://games.caputchin.com, 'unsafe-inline' et 'wasm-unsafe-eval' si tu n'utilises pas de jeux. Retire 'unsafe-eval' si tu désactives l'instrumentation. Remplace https://cdn.jsdelivr.net par ta propre origine, dans script-src comme dans connect-src, si tu héberges le widget toi-même.
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.