セキュリティ
利用者は自分のコードを自分のブラウザで動かします。ただし、npm の依存やサイトに入っている外のスクリプトが悪いものかもしれません。OpenVessel が守るのは、埋め込む側のページ、利用者の知らない外への通信、プレビューからのランタイムの操作の3つです。
オリジン分離と信頼境界
| オリジン | 載るもの | 信頼レベル |
|---|---|---|
| H 埋め込む側のページ | 埋め込む側のアプリ・OpenVessel の API | いちばん信用する |
| R ランタイム | カーネル・プロセス・npm の依存・利用者のコード | 利用者の持ち物として扱う。外への通信だけネットワークゲートを通す |
| P プレビュー | サイトの HTML・JavaScript、サイトが読む外のスクリプト | 信用しない |
3つは必ず別のオリジンに置きます。多人数で使うサービスでは、プレビューをセッションごとのサブドメインにして、プレビュー同士も分けます。
対策
ホストページの保護
- ›R と P は別オリジンの iframe。H の DOM・クッキー・ストレージに届かない
- ›P の iframe は sandbox(トップのページを移動させられない)
- ›ランタイムは CSP の frame-ancestors で H だけに絞り、起動時に埋め込み元も確かめる
外向き通信の制御
- ›R の文書と Worker は CSP connect-src 'self'。プロセスの fetch・WebSocket は外へ直接出られない
- ›出口は R の Service Worker の1か所だけ。方針で許したものだけを本物の fetch で出す
- ›方針を変えられるのは H だけ。Worker からの「許可」は受け付けない
- ›deny はいつも優先。http: と https: 以外は出さない
- ›P の外への読み込みも、P の Service Worker が同じ方針で絞る
プレビューからの操作の遮断
- ›P から使える口は、仮想のサーバーへの HTTP と WebSocket だけ
- ›ファイルの読み書き・プロセスの起動・通信の許可は、P からはできない
- ›中継の iframe を乗っ取られても、同じ口しか無い
- ›許可のダイアログの連打は、同じ相手への問い合わせをまとめる
npm サプライチェーン対策
- ›依存パッケージの install スクリプト(preinstall・postinstall など)は動かさない
- ›プロジェクト自身の postinstall・prepare は npm と同じく動かす(--ignore-scripts で止まる)
- ›tarball は integrity(sha512)を確かめてから展開する。キャッシュから出すときも確かめる
- ›版の一覧(packument)はセッションのメモリだけに持ち、ストレージに残さない
ネットワークゲート(外向き通信の制御)
インターネットへ出る唯一の道です。何を許すかは、埋め込む側のページが決めます。用意したセット(npm・fonts・icons・cdn・github)と、allow・deny の一覧と、モードを組み合わせます。
network: {
mode: 'ask',
presets: ['npm', 'fonts', 'icons', 'cdn'],
},
onPermissionRequest: (req) =>
confirm(`Allow ${req.host}?`) ? 'session' : 'deny', ask 用意したセット以外は、相手ごとに利用者に確かめる(このセッションだけ許す・断る)
allowlist 用意したセットと allow に書いた相手だけ。ほかは断る
allow-all すべて許す(deny に書いた相手は除く)
外への通信は、ブラウザの fetch で出します。TLS と証明書の確認はブラウザが行います。
脅威モデルの対象外
次のものは、OpenVessel の守りの範囲に入れていません。設計の前提として、はっきりさせておきます。
- ランタイムの中のコード同士の分離(利用者のコードと依存は同じ箱で動く。本物の Node と同じ)
- CPU・メモリの使い過ぎ(ブラウザのタブの範囲で止まる。タブを閉じれば止まる)
- 利用者自身が「全部許す」にしたあとの、外への通信
脆弱性は、公開の issue ではなく、リポジトリの Security Advisories から非公開で報告してください。