Caputchin이 게임을 샌드박싱하는 방법
Caputchin 게임은 방문자 브라우저에서, 페이지에서 도는 제3자 코드입니다. 그것이 마켓플레이스에서 오든 직접 호스팅하든, Caputchin은 그것을 기본으로 신뢰되지 않는 것으로 다루고 여러 독립 격리 계층으로 감싸니, 버그가 있거나 적대적인 게임도 렌더링하고, 플레이하고, 결과를 보고할 수 있지만, 그 밖에는 아무것도 닿을 수 없습니다: 페이지도, 방문자 데이터도, 네트워크도.
이 페이지는 모든 조치, 각각이 왜 존재하는지, 그리고 그 전체가 당신 자신의 사이트에 더하는 CSP와 어떻게 상호작용하는지 설명합니다.
한 줄로 된 위협 모델
게임은 제3자가 공급한 임의의 JavaScript(그리고 어쩌면 WebAssembly)를 돌립니다. 따라서 격리 목표는: 게임은 연산하고 그릴 수 있고, 결과를 위젯에 돌려줄 수 있지만, 자기 프레임 바깥의 어떤 것도 읽거나 영향을 줄 수 없다. 아래 모든 계층이 그 하나의 목표를 섬기며, 그것들은 심층 방어입니다, 어떤 단일 계층도 완벽하다고 신뢰되지 않습니다.
계층 1: 불투명 오리진 샌드박스 iframe
모든 게임은 위젯이 일부러 최소한의 sandbox 속성으로 짓는 <iframe> 안에서 돕니다. 부여되는 유일한 토큰은 allow-scripts인데, 게임이 JavaScript이고 돌아야 하기 때문입니다. 그 밖의 모든 것은 보류되며, 가장 중요한 누락은 allow-same-origin입니다.
allow-same-origin 없이 브라우저는 프레임에 고유한 null 오리진을 줍니다. 그 하나의 사실이 하중을 견디는 경계입니다:
- 게임은 페이지의 DOM, 쿠키,
localStorage, 또는 Caputchin 세션을 읽을 수 없습니다, 그것은 당신의 것과 낯선 일회용 오리진에 봉인되어 있습니다. - 프레임은 또한 최상위 창을 결코 탐색하거나, 팝업을 열거나, 폼을 제출하거나, 네이티브 다이얼로그를 띄울 수 없으므로(그 모든 역량은 Caputchin이 부여하지 않는 샌드박스 토큰입니다), 게임에서 페이지로 도로 나가는 경로가 없습니다.
**allow-same-origin 없는 allow-scripts**의 조합이 요점 전체입니다: 브라우저는 게임의 코드를 실행하지만 프레임을 당신의 것을 아무것도 건드릴 수 없는 낯선 오리진으로 다룹니다. Caputchin은 게임 프레임에 결코 allow-same-origin을 더하지 않습니다. (그 결과로, 게임이 위젯에 post하는 모든 메시지는 오리진 "null"에서 도착하며, 위젯의 채널 확인이 그것을 기대합니다, null이 아닌 오리진은 그 자체로 잘못 구성된 샌드박스를 신호할 것입니다.)
계층 2: 프레임의 문서는 가져와지는 게 아니라 인라인으로 지어짐
위젯은 iframe을 원격 페이지로 향하게 하지 않습니다. 그것은 프레임의 전체 HTML 문서를 인라인으로(srcdoc를 통해) 구성해 null 오리진 프레임에 건넵니다. 그 문서는 작습니다: 엄격한 CSP 메타 태그(다음 계층), 작은 Caputchin 런타임 부트스트랩, 그리고 게임 번들을 로드하는 하나의 <script> 태그.
이것은 두 전달 경로 모두에 같은 기제이며, 번들 URL만 다릅니다:
| 게임 소스 | 인라인 문서의 번들 URL |
|---|---|
| 마켓플레이스 (플랫폼 해소) | Caputchin이 마운트 시 해소한, 고정되고 무결성 해시된 번들 URL. |
자체 호스팅 (game-src) | 공급한 URL. |
문서가 게임이 아니라 위젯에 의해 작성되므로, 게임은 CSP도, 런타임도, 프레임의 구조도 결코 제어하지 않으며, 로드된 뒤 자기 번들이 하는 것만 제어합니다.
계층 3: 마켓플레이스 게임을 위한 Subresource Integrity
Caputchin이 마켓플레이스 게임을 낼 때 그것은 번들을 불변 버전으로 고정하고 그것의 암호 해시(SHA-384)를 기록합니다. 번들을 로드하는 <script> 태그는 그 해시를 crossorigin="anonymous"와 함께 integrity 속성으로 지니니, 단 한 바이트라도 고정된 것과 다르면 브라우저 자체가 번들 실행을 거부합니다. 손상된 CDN은 다른 코드를 대체할 수 없습니다; 로드가 닫힌 채 실패합니다.
자체 호스팅 게임에는 플랫폼이 주장하는 해시가 없으니(당신이 당신 자신의 바이트의 신뢰 뿌리입니다), integrity 속성은 그 경로에서 그저 생략됩니다. 마켓플레이스 게임의 결과가 서버에서 독립적으로 다시 도출되는 방식, 무결성과는 별개의 보장은 리플레이 계약을 보세요.
계층 4: 엄격한 인라인 Content-Security-Policy
인라인 문서는 빡빡한 Content-Security-Policy를 지닙니다. 프레임이 이미 null 오리진이고 샌드박스됐어도, CSP는 로드된 코드가 무엇을 할 수 있는지 더 옥죕니다, 그것은 기본으로 모든 것을 거부하고 최소한만 다시 부여합니다:
| 지시어 | 값 | 왜 |
|---|---|---|
default-src | 'none' | 아래에서 명시적으로 허용되지 않은 모든 것을 거부. |
script-src | Caputchin 자체 인라인 런타임의 해시 + 게임 번들의 오리진 + 'wasm-unsafe-eval' | Caputchin의 정확한 부트스트랩('unsafe-inline'이 아니라 sha256- 해시로 고정)과 게임 자체의 번들만 실행. 'wasm-unsafe-eval'은 WASM 엔진이 전체 'unsafe-eval'을 부여하지 않고 컴파일하게 함. |
connect-src | 'none' | 빼내기 방지 지시어. 게임은 어디로도 fetch, XHR, WebSocket 열기, 또는 비콘을 할 수 없음. 네트워크 출구 전혀 없음. |
img-src | data: (더해 모든 스킨 자산 오리진, 아래) | 스프라이트는 인라인, 또는 허용된 자산 호스트에서. |
media-src | data: (더해 모든 스킨 자산 오리진, 아래) | 오디오/비디오는 인라인, 또는 허용된 자산 호스트에서. |
font-src | data: | 인라인 폰트만. |
style-src | 'unsafe-inline' | 게임은 인라인 스타일을 설정; 외부 스타일시트 없음. (스타일은 스크립트나 네트워크처럼 데이터를 빼낼 수 없음.) |
순효과: 게임은 돌고, 렌더링하고, SDK 브리지를 통해 결과를 보고하며, 그 밖에는 아무것도, 특히 네트워크에는 닿을 수 없습니다. 이 CSP는 Caputchin이 설정하며 당신이 구성하는 것이 아닙니다; 게임이 정확히 얼마나 갇혀 있는지 이해하도록 여기 나열되어 있습니다.
고객 설정이 프레임 CSP를 넓히는 한 곳
스킨은 이미지나 오디오 필드를 당신 자신의 CDN의 절대 URL로 가리킬 수 있습니다(스킨을 보세요). 그 자산이 잠긴 프레임 안에서 로드되려면, Caputchin은 프레임의 img-src / media-src에 그 정확한 자산 오리진만 더하고, 그 밖에는 어디에도 더하지 않습니다. script-src와 connect-src는 결코 넓혀지지 않으니, 허용된 자산 오리진이라도 코드를 로드하거나 네트워크 채널을 여는 데 쓸 수 없습니다. 이것이 구성된 값이 프레임의 정책에 영향을 주는 단 하나의, 좁은 방법이며, 그것도 여전히 자산뿐입니다.
계층 5: 단방향 결과 채널
게임은 페이지로 호출 가능한 표면이 없습니다. 나가는 유일한 방법은 SDK 브리지입니다: 게임은 오리진에 사는 위젯으로 자기 불투명 결과(그 트레이스)를 postMessage하는 단일 메서드를 호출합니다. 위젯이 Caputchin의 API와 이야기하는 것입니다; 게임은 결코 그러지 않습니다(그럴 수 없습니다, connect-src가 'none'입니다). 그러니 신뢰 경로는 게임 → 위젯 → 백엔드이고, 각 도약은 결과를 앞으로만 넘기지, 결코 게임에 뒤로의 도달을 부여하지 않습니다.
당신 자신의 페이지에 설정하는 CSP
위 계층은 당신과 방문자를 게임으로부터 보호합니다. 당신 자신의 페이지의 Content-Security-Policy는 다른, 보완하는 통제입니다: 그것은 페이지를 모든 것으로부터 보호합니다.
이 절의 모든 것을 이끄는 사실이 하나 있습니다: 게임 프레임은 당신 페이지의 정책을 물려받습니다. 프레임은 인라인 srcdoc 문서이고, 브라우저는 그런 문서에 프레임 자신의 정책 위로 임베드하는 페이지의 CSP를 겹쳐 적용합니다. 그러니 위에 보인 프레임의 잠긴 정책은 Caputchin이 더하는 바닥이지, 당신 정책의 대체가 아닙니다. 프레임 안에서 도는 것은 둘 다 만족해야 하고, 그 말은 당신 페이지의 정책이 게임 번들과 프레임 자신의 부트스트랩을 허용해야 한다는 뜻입니다. 둘 다 당신 페이지가 직접 로드하는 것이 아닌데도 말입니다.
최소한, 위젯에 필요한 것:
| 지시어 | 허용 | 왜 |
|---|---|---|
script-src | 위젯 스크립트를 로드하는 오리진, 더해 'unsafe-eval' | <caputchin-widget> / <caputchin-game>을 정의하는 <script>, 고른 CDN(jsDelivr, 당신 자신의 호스트, 또는 caputchin.com). 'unsafe-eval'은 계측 챌린지가 돌게 함; 계측이 켜진 동안 필수이며, 키의 보안 페이지에서 계측을 꺼서 떨굴 수 있음. |
script-src (게임 전용) | 게임 번들의 오리진, 더해 'unsafe-inline'과 'wasm-unsafe-eval' | 프레임이 이 정책을 물려받기 때문. 번들 오리진은 마켓플레이스 게임이면 https://games.caputchin.com, 자체 호스팅 게임이면 당신 자신의 호스트나 CDN. 'unsafe-inline'은 프레임 자신의 부트스트랩 스크립트를 덮음: 프레임은 그것을 sha256- 해시로 고정하지만 당신 페이지는 고정할 수 없는데, 그 해시가 위젯 릴리스마다 바뀌기 때문. 'wasm-unsafe-eval'은 WebAssembly 엔진 위에 지은 게임이 필요로 함. 게임 없이 <caputchin-widget>만 쓴다면 어느 것도 해당하지 않음. |
connect-src | https://verify.caputchin.com, 더해 위젯 스크립트를 로드하는 오리진 | 서로 다른 두 호출. 위젯은 검증을 세우고 확인하려고 Caputchin 검증 서비스를 호출하니 정확히 그 오리진을 허용 (위젯을 다른 API 호스트로 향하게 하면, 대신 그것을 허용). proof-of-work 풀이기는 거기에 더해 위젯 스크립트 옆에서 자기 WebAssembly 모듈을 가져오니, 그 CDN 오리진은 script-src만큼이나 여기에도 필요. 위젯을 당신 자신의 오리진에 호스팅하면 두 번째는 'self'로 덮임. |
worker-src | blob: | proof-of-work 풀이기는 전적으로 blob: URL에서 만들어진 Web Worker에서 돌고, 메인 스레드 대체가 없으니, 이것은 필수. worker-src를 생략하면, 브라우저는 child-src 다음 default-src로 물러나고, default-src 'self'만으로는 blob:을 허용하지 않음. |
proof-of-work 풀이기가 빠른 WebAssembly로 돌게 하려면 script-src에 'wasm-unsafe-eval'을 더하세요. 그 키워드도, 풀이기가 모듈을 가져오는 오리진도 검증이 성공하는 데 필수는 아닙니다: 둘 중 하나라도 빠지면 풀이기는 더 느린 순수 JavaScript 구현(여전히 Worker 안)으로 물러나고 검증은 여전히 동작합니다. 다만 둘은 함께여야 값을 합니다. 풀이기는 WebAssembly 모듈을 fetch한 다음 컴파일하니, 빠른 경로에는 connect-src의 위젯 스크립트 오리진 과 script-src의 'wasm-unsafe-eval'이 둘 다 필요합니다; 하나만 허용하면 느린 풀이기에 남게 되고, 왜인지 말해 주는 것은 콘솔에 아무것도 없습니다.
게임을 위해 frame-src 항목이 필요하지 않습니다. 게임 프레임은 URL에서 가져온 문서가 아니라 인라인 srcdoc 문서라서, frame-src가 맞춰 볼 URL 자체가 없고, frame-src 'none' 아래에서도 프레임은 로드됩니다.
정말로 필요한 것, 그리고 사람들을 놀라게 하는 것은 당신 페이지의 **script-src**에 들어가는 게임 번들의 오리진입니다. 당신 페이지가 그 번들을 결코 로드하지 않는데도 말입니다: 로드하는 것은 프레임이고, 프레임은 자기 정책에 더해 당신 정책 아래에서도 돕니다. 프레임의 부트스트랩과 WebAssembly 게임 엔진도 마찬가지입니다. 그것들을 허용해도 게임이 닿을 수 있는 범위가 넓어지지는 않습니다: 프레임 자신의 정책은 여전히 connect-src 'none'을 고정하고, 프레임은 여전히 불투명 오리진이며, 당신 페이지의 허용이 프레임이 스스로에게 건 바닥을 느슨하게 할 수는 없습니다.
script-src의 'unsafe-inline'은 그 목록에서 불편한 대목이고, 그렇게 말하는 편이 정직합니다: 오늘 게임을 돌리려면 필수이고, 당신 자신의 페이지가 가진 인라인 스크립트 방어를 약하게 하며, 대안(프레임 부트스트랩의 sha256- 해시를 고정하기)은 그 해시가 위젯 릴리스마다 움직이는 동안 현실적이지 않습니다. 어떤 페이지에서 그 맞바꿈이 받아들일 만하지 않다면, 거기서는 <caputchin-widget>을 쓰세요. 게임 전용 행의 어떤 것도 필요하지 않습니다.
Caputchin 요소가 CSP 아래에서 조용히 로드에 실패하면, 브라우저 콘솔이 차단된 지시어의 이름을 댑니다; 그것이 대는 오리진이나 키워드를 그 지시어에 더하세요. 엄격하게 시작하고 콘솔이 청하는 것만 정확히 여세요. 그리고 about:srcdoc에 대해 보고된 위반도 당신 자신의 페이지에 대한 위반만큼 꼼꼼히 읽으세요: 그것들은 게임 프레임이 물려받은 당신 정책에 부딪힌 것이고, 사람들이 놓치는 것이 바로 그것들입니다. 워커를 위해 blob:(worker-src)이, 그리고 계측이 켜진 동안 script-src에 'unsafe-eval'이 필요합니다; 'unsafe-eval' 요건은 키의 보안 페이지에서 계측을 끄면 사라집니다. 'unsafe-inline'은 위 표대로 게임을 돌릴 때만 필요합니다. proof-of-work의 빠른 경로는 선택이지만 짝을 이룹니다: script-src의 'wasm-unsafe-eval' 과 connect-src의 위젯 스크립트 오리진. 이 짝이야말로 콘솔이 도와주지 않는 유일한 경우인데, JavaScript 풀이기로 물러나는 것은 오류가 아니기 때문입니다.
실제로 통하는 정책
위젯을 jsDelivr에서 로드하고, 마켓플레이스 게임을 쓰고, 계측을 켠 경우:
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'을 떨구세요. 위젯을 자체 호스팅하면 script-src와 connect-src 양쪽에서 https://cdn.jsdelivr.net을 당신 자신의 오리진으로 바꾸세요.
왜 이렇게 많은 계층인가
각 계층은 그 자체로 의미 있는 경계일 것입니다; 함께 그것들은 하나의 실패가 침해가 아니라는 뜻입니다. null 오리진만으로도 이미 게임을 데이터로부터 벽으로 막고; CSP만으로도 이미 네트워크 출구를 죽이고; 무결성만으로도 이미 정확한 바이트를 고정합니다. Caputchin은 그 모두를 돌리는데, 신뢰되지 않는 제3자 코드가 바로 심층 방어가 제값을 하는 곳이기 때문입니다.
함께 보기
- 게임이 봇에 저항하는 방법: 게임 무결성의 서버 권위 측면.
- 리플레이 계약: 게임의 결과가 서버 측에서 독립적으로 다시 도출되는 방식.
- 자체 호스팅 게임 빌드하기: 이 샌드박스가 작성자에게 부과하는 번들 제약.
- CDN에서 위젯 로드하기: 페이지 CSP가 허용해야 하는 스크립트 오리진 고르기.