Caputchin
Понимание Caputchin

Как Caputchin изолирует игры в песочнице

Открыть как Markdown

Игра Caputchin представляет собой сторонний код, который работает в браузере твоего посетителя, на твоей странице. Приходит ли она из Marketplace или ты размещаешь её сам, Caputchin трактует её как не доверенную по умолчанию и оборачивает в несколько независимых слоёв изоляции, так что баговая или враждебная игра может отрендериться, сыграть и отчитаться о результате, но не может дотянуться ни до чего другого: ни до твоей страницы, ни до данных твоего посетителя, ни до сети.

Эта страница объясняет каждую меру, почему каждая существует и как всё это взаимодействует с CSP, который ты добавляешь на свой собственный сайт.

Модель угроз в одну строку

Игра запускает произвольный JavaScript (и возможно WebAssembly), поставленный третьей стороной. Цель изоляции поэтому такова: игра может вычислять и рисовать, и она может вернуть результат виджету, но не может прочитать или повлиять ни на что вне своего собственного кадра. Каждый слой ниже служит этой одной цели, и они образуют эшелонированную защиту, ни один отдельный слой не считается совершенным.

Слой 1: iframe в песочнице с непрозрачным источником

Каждая игра работает внутри <iframe>, который виджет строит с намеренно минимальным атрибутом sandbox. Единственным выданным токеном служит allow-scripts, потому что игра написана на JavaScript и должна работать. Всё остальное удержано, а важнее всего упущен allow-same-origin.

Без allow-same-origin браузер даёт кадру уникальный, нулевой источник. Этот один факт задаёт несущую границу:

  • Игра не может прочитать DOM, cookie, localStorage твоей страницы или сессию Caputchin, она запечатана в одноразовый источник, чуждый твоему.
  • Поскольку кадр также никогда не может навигировать твоё верхнее окно, открывать попапы, отправлять формы или всплывать нативные диалоги (все эти возможности представляют собой токены песочницы, которые Caputchin не выдаёт), нет пути от игры обратно наружу к твоей странице.

Сочетание allow-scripts без allow-same-origin и есть вся суть: браузер выполняет код игры, но трактует кадр как чужой источник, который не может коснуться ничего твоего. Caputchin никогда не добавляет allow-same-origin к игровому кадру. (Как следствие, каждое сообщение, которое игра постит виджету, приходит от источника "null", чего проверки канала виджета и ожидают, не-нулевой источник сам сигналил бы о неверно настроенной песочнице.)

Слой 2: документ кадра строится инлайн, а не запрашивается

Виджет не наводит iframe на удалённую страницу. Он конструирует весь HTML-документ кадра инлайн (через srcdoc) и вручает его кадру с нулевым источником. Этот документ крошечный: строгий мета-тег CSP (следующий слой), маленький бутстрап рантайма Caputchin и один тег <script>, который загружает бандл игры.

Это один и тот же механизм для обоих путей доставки, различается только URL бандла:

Источник игрыURL бандла в инлайновом документе
Marketplace (разрешено платформой)Закреплённый URL бандла с хешем целостности, который Caputchin разрешил при монтировании.
Самостоятельно размещённый (твой game-src)URL, который ты поставил.

Поскольку документ авторствует виджет, а не игра, игра никогда не контролирует CSP, рантайм или структуру кадра, только то, что делает её собственный бандл после загрузки.

Слой 3: Subresource Integrity для игр из Marketplace

Когда Caputchin отдаёт игру из Marketplace, он прибивает бандл к неизменной версии и записывает её криптографический хеш (SHA-384). Тег <script>, который загружает бандл, несёт этот хеш как атрибут integrity с crossorigin="anonymous", так что сам браузер отказывается выполнять бандл, если хоть один байт отличается от закреплённого. Скомпрометированный CDN не может подставить другой код; загрузка падает закрыто.

У самостоятельно размещённой игры нет утверждённого платформой хеша (ты корень доверия для своих собственных байтов), так что атрибут integrity для этого пути просто опускается. Смотри контракт реплея о том, как результат игры из Marketplace независимо перевыводится на сервере, отдельная гарантия от целостности.

Слой 4: строгая инлайновая Content-Security-Policy

Инлайновый документ несёт тугую Content-Security-Policy. Хотя кадр уже с нулевым источником и в песочнице, CSP дополнительно ограничивает, что может делать загруженный код, она отрицает всё по умолчанию и заново выдаёт только минимум:

ДирективаЗначениеПочему
default-src'none'Отрицать всё, что явно не разрешено ниже.
script-srcхеш собственного инлайнового рантайма Caputchin + источник бандла игры + 'wasm-unsafe-eval'Запускать только точный бутстрап Caputchin (закреплённый хешем sha256-, а не 'unsafe-inline') и собственный бандл игры. 'wasm-unsafe-eval' даёт движку WASM компилировать без выдачи полного 'unsafe-eval'.
connect-src'none'Анти-эксфильтрационная директива. Игра не может fetch, XHR, открыть WebSocket или отправить маяк куда-либо. Никакого сетевого выхода вообще.
img-srcdata: (плюс любые источники ассетов скина, ниже)Спрайты инлайн или с разрешённого хоста ассетов.
media-srcdata: (плюс любые источники ассетов скина, ниже)Аудио/видео инлайн или с разрешённого хоста ассетов.
font-srcdata:Только инлайновые шрифты.
style-src'unsafe-inline'Игры задают инлайновые стили; никаких внешних таблиц стилей. (Стили не могут эксфильтровать данные так, как скрипт или сеть.)

Чистый эффект: игра работает, рендерится и отчитывается о результате через мост SDK и не может дотянуться ни до чего другого, особенно не до сети. Этот CSP задаёт Caputchin, и это не то, что ты настраиваешь; он перечислен здесь, чтобы ты в точности понимал, насколько игра заключена.

Единственное место, где клиентская настройка расширяет CSP кадра

Скины могут навести поля изображения или аудио на абсолютный URL на твоём собственном CDN (смотри скины). Чтобы эти ассеты загрузились внутри запертого кадра, Caputchin добавляет только эти точные источники ассетов в img-src / media-src кадра, и больше никуда. script-src и connect-src никогда не расширяются, так что даже разрешённый источник ассета нельзя использовать для загрузки кода или открытия сетевого канала. Это единственный, узкий способ, которым настроенное значение влияет на политику кадра, и это всё равно только для ассетов.

Слой 5: односторонний канал результата

У игры нет вызываемой поверхности в твою страницу. Единственным выходом служит мост SDK: игра вызывает один метод, который через postMessage шлёт свой непрозрачный результат (свою трассу) виджету, который живёт на твоём источнике. Виджет это то, что говорит с API Caputchin; игра никогда не говорит (она не может, connect-src установлен в 'none'). Так что путь доверия таков: игра → виджет → твой бэкенд, и каждый прыжок только передаёт результат вперёд, никогда не выдаёт игре досягаемость назад.

CSP, который ты задаёшь на своей собственной странице

Слои выше защищают тебя и твоего посетителя от игры. Content-Security-Policy на твоей собственной странице представляет собой другой, дополняющий рычаг: он защищает твою страницу от всего.

Один факт правит всем в этом разделе: игровой кадр наследует политику твоей страницы. Кадр представляет собой инлайновый документ srcdoc, а к таким документам браузеры применяют CSP встраивающей страницы поверх собственной политики кадра. Так что запертая политика кадра, показанная выше, задаёт пол, который добавляет Caputchin, а не замена твоей. Всё, что работает внутри кадра, должно удовлетворять обеим, а значит политика твоей страницы обязана разрешить бандл игры и собственный бутстрап кадра, хотя ни то ни другое твоя страница напрямую не грузит.

Как минимум, виджету нужно:

ДирективаРазрешитьПочему
script-srcисточник, с которого ты загружаешь скрипт виджета, плюс 'unsafe-eval'<script>, который определяет <caputchin-widget> / <caputchin-game>, CDN, который ты выбрал (jsDelivr, твой собственный хост или caputchin.com). 'unsafe-eval' даёт работать испытанию инструментирования; оно требуется, пока инструментирование включено, и ты можешь убрать его, выключив инструментирование на странице Безопасность ключа.
script-src (только для игр)источник бандла игры, плюс 'unsafe-inline' и 'wasm-unsafe-eval'Потому что кадр наследует эту политику. Источник бандла: https://games.caputchin.com для игр из Marketplace, либо твой собственный хост / CDN для самостоятельно размещённой игры. 'unsafe-inline' покрывает собственный бутстрап-скрипт кадра, который кадр закрепляет хешем sha256-, а твоя страница закрепить не может, потому что этот хеш меняется с каждым релизом виджета. 'wasm-unsafe-eval' нужен играм, построенным на движке WebAssembly. Ничего из этого не касается тебя, если ты используешь только <caputchin-widget> без игры.
connect-srchttps://verify.caputchin.com, плюс источник, с которого ты загружаешь скрипт виджетаЭто два разных вызова. Виджет обращается к сервису проверки Caputchin, чтобы настроить и подтвердить проверку, так что разреши именно этот источник (если ты наводишь виджет на другой хост API, разреши его вместо этого). Решатель proof-of-work вдобавок забирает свой модуль WebAssembly оттуда же, где лежит скрипт виджета, так что источнику CDN место и здесь, и в script-src. Если ты размещаешь виджет на своём собственном источнике, второй покрывается через 'self'.
worker-srcblob:Решатель proof-of-work работает целиком в Web Workers, созданных из blob:-URL, без отката на основной поток, так что это обязательно. Если ты опускаешь worker-src, браузеры откатываются на child-src, затем default-src, а default-src 'self' сам по себе не разрешает blob:.

Добавь 'wasm-unsafe-eval' в script-src, чтобы решатель proof-of-work работал как быстрый WebAssembly. Ни это ключевое слово, ни источник, откуда решатель забирает модуль, не требуются, чтобы проверка прошла: если не хватает любого из двух, решатель откатывается на более медленную реализацию на чистом JavaScript (всё ещё внутри Worker), и проверка всё равно работает. Но окупаются они только вместе. Решатель делает fetch модуля WebAssembly, а потом компилирует его, так что быстрому пути нужен источник скрипта виджета в connect-src и 'wasm-unsafe-eval' в script-src; разрешив одно без другого, ты остаёшься на медленном решателе, и консоль ничем не объяснит почему.

Тебе не нужна запись frame-src для игры. Игровой кадр представляет собой инлайновый документ srcdoc, а не документ, забранный по URL, так что никакого URL, с которым frame-src мог бы сопоставиться, нет, и кадр грузится даже под frame-src 'none'.

А что тебе действительно нужно и что застаёт людей врасплох, это источник бандла игры в script-src твоей страницы, хотя сама страница этот бандл никогда не грузит: его грузит кадр, а кадр работает под твоей политикой в придачу к своей. То же касается бутстрапа кадра и движка игры на WebAssembly. Выдача этих разрешений не расширяет то, куда игра может дотянуться: собственная политика кадра по-прежнему закрепляет connect-src 'none', кадр по-прежнему с непрозрачным источником, и разрешение твоей страницы не ослабит пол, который кадр ставит себе сам.

'unsafe-inline' в script-src составляет неудобную часть этого списка, и честно будет это сказать: сегодня он обязателен, чтобы запустить игру, он ослабляет защиту твоей собственной страницы от инлайновых скриптов, а альтернатива (закрепить хеш sha256- бутстрапа кадра) непрактична, пока этот хеш движется с каждым релизом виджета. Если такой размен для какой-то страницы неприемлем, используй там <caputchin-widget>: ему не нужно ничего из строки «только для игр».

Если элемент Caputchin молча не загружается под твоим CSP, твоя консоль браузера называет заблокированную директиву; добавь источник или ключевое слово, которое она называет, в эту директиву. Начни строго и открой ровно то, что просит консоль, и читай нарушения, сообщённые для about:srcdoc, так же внимательно, как и те, что для твоей собственной страницы: это игровой кадр упирается в твою унаследованную политику, и именно их люди пропускают. Тебе нужен blob: для воркеров (worker-src) и, пока инструментирование включено, 'unsafe-eval' в script-src; требование 'unsafe-eval' уходит, если ты выключаешь инструментирование на странице Безопасность ключа. 'unsafe-inline' нужен тебе только чтобы запускать игры, по таблице выше. Быстрый путь proof-of-work опционален, но парный: 'wasm-unsafe-eval' в script-src и источник скрипта виджета в connect-src. Эта пара представляет собой единственный случай, где консоль тебе не помощник, ведь откат на JavaScript-решатель не считается ошибкой.

Политика, которая работает

Виджет грузится с jsDelivr, игры из Marketplace, инструментирование включено:

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'

Убери https://games.caputchin.com, 'unsafe-inline' и 'wasm-unsafe-eval', если ты не используешь игры. Убери 'unsafe-eval', если выключаешь инструментирование. Замени https://cdn.jsdelivr.net на свой собственный источник и в script-src, и в connect-src, если размещаешь виджет сам.

Почему так много слоёв

Каждый слой был бы значимой границей сам по себе; вместе они значат, что сбой в одном не означает пробоя. Один нулевой источник уже отгораживает игру от твоих данных; один CSP уже убивает сетевой выход; одна целостность уже прибивает точные байты. Caputchin запускает их все, потому что не доверенный сторонний код это ровно то место, где эшелонированная защита оправдывает себя.

См. также

На этой странице