セキュリティ
ページとアクションが訪問者の送るものに耐えるように hypersig が確かめること、ブラウザから届くデータがサーバーのコードを動かせない理由、アプリに任せることをまとめます。
ページに書き出すもの
| 起こりうること | hypersig がすること |
|---|---|
| 値がマークアップやスクリプトになる | サーバーはテキストと属性の値をエスケープして書き、ランタイムはテキストを textContent で設定します。ビルダーが書いていない HTML を差し込む公開のメソッドはありません。 |
javascript: のようなリンクがスクリプトを動かす | El::href と El::src はルートへの Link か SafeUrl を受け取ります。SafeUrl が持てるのは http:、https:、mailto: か相対 URL だけです。訪問者が入力した URL は SafeUrl::parse で確かめ、シグナルの SafeUrl のフィールドはデシリアライズのときにもう一度確かめます。attr と bind_attr は href、src、on* のハンドラ、srcdoc、action、formaction、xlink:href、data、http-equiv を大文字と小文字を区別せずに断り、英数字、-、_、.、: 以外を含む名前も断って、パニックします。attributeName が href の SVG のアニメーションもパニックし、bind_attr は attributeName を書きません。 |
| URL がブラウザを別のホストへ送る | Url は Route::url からしか作れません。Rest の値の先頭の / と続いた / は %2F と書きます。最初のセグメントが空でパスが // で始まるときは前に /. を付けるので、ルートが書く URL とリンクが別のホストを指すことはありません。 |
| スタイルの値が宣言や規則を足す | CSS の文字列は制御文字、"、\、< をすべてエスケープします。束縛したスタイルの値のうち宣言を終わらせうるものはランタイムに任せ、ランタイムの setProperty がそれを断ります。raw はエスケープ、コメント、;、{、}、< と、閉じていない引用符や括弧を断ります。Stylesheet::new は </style をコンパイル時に断ります。 |
| ランタイムがページにあるコードを動かす | ランタイムは決まった演算子の表だけで式を評価します。eval も new Function も呼ばないので、Page::csp_nonce を付けたページは、unsafe-eval も unsafe-inline もない Content-Security-Policy のもとで動きます。 |
リクエストが満たすこと
アクション、ストリーム、アップロードは、次のリクエストにハンドラが動く前にエラーを返します。そのため、別のサイトのページが、訪問者のブラウザに訪問者の Cookie を付けて呼ばせることはできません。
| リクエスト | 応答 |
|---|---|
Sec-Fetch-Site が same-origin でない(same-site も含む) | 403 |
本文が application/json でない POST。フォームはこの形式を送れず、別のサイトのスクリプトはプリフライトのあとでしか送れず、このサーバーはプリフライトに答えません | 415 |
<img> やリンクからの GET のように、ランタイムが送る Accept: text/event-stream のない GET | 403 |
プリフライトが要る hypersig-upload のヘッダーのないアップロード | 403 |
Upload::max_bytes(指定しなければ 8 MiB)より長いアップロード | 413 |
| アクションの引数の型にデシリアライズできない引数 | 400 |
別のサイトのスクリプトは GET にこの Accept を付けて送れます。Sec-Fetch-Site を送らないブラウザではそれが通るので、GET のアクションでは何も変更しないでください。
ブラウザから届くデータとサーバーのコード
フレームワークによっては、リクエストでサーバーのコードを動かせたことがあります。リクエストがサーバーの関数、モジュール、クラスを名指しし、フレームワークがリクエストを読みながらそれを探して呼ぶ仕組みでした。hypersig では次の理由で、リクエストがコードを名指しする手段がありません。
- どのハンドラが動くかは、ルーターがリクエストのパスとメソッドから決め、ルートはプログラムの起動時に決まっています。リクエストがルートを足したり、名前でハンドラを選んだりすることはできません。
- リクエストが運ぶのは JSON の引数だけです。
serde_jsonが、アクションがコンパイル時に宣言した引数の型に読み込み、合わないものには 400 を返します。この JSON には、関数、サーバーのオブジェクトへの参照、組み立てるクラスを書く形がないので、読み込みはデータを作るだけで、何も呼びません。 serde_jsonは入れ子が 128 段を超えると読むのをやめるので、深く入れ子にした本文はスタックを使い切る前に断られます。- サーバーが評価するのは、最初の HTML を書くためにアプリの Rust のコードが組み立てた式だけです。リクエストで届いた式やテンプレートを評価することも、リクエストが指したファイルを読むこともありません。
- クレートは
unsafeを禁じているので、不正なリクエストでクレートのメモリを壊すことはできません。
アプリが確かめること
- 訪問者が誰で、何をしてよいか。アクションの URL はページを読み込まずに直接呼べ、ブラウザは引数の型の値なら何でも送れるので、各ハンドラが、訪問者がその引数を扱ってよいかを確かめてください。
authの例はハンドラごとにエクストラクタでサインイン中のメンバーを読みます。 - シグナルの値。ブラウザが持っていて書き換えられるので、サーバーが頼る価格、役割、持ち主は、サーバーで読み直してください。
Content-Security-Policyのヘッダー。Page::csp_nonceはページの要素に nonce を付けます。ヘッダーはアプリが送り、nonce はレスポンスごとに 128 ビット以上の乱数にしてください。- URL のホスト。
SafeUrlはどのホストでも持てるので、訪問者が入力した URL は訪問者がたどるリンクに使い、スクリプトのsrcや<base>のhrefには使わないでください。 - 保存したデータから作る id。固定の id が 2 つ同じなら描画はパニックしますが、
Stringの id が 2 つ同じでもそのまま描画され、その id へのリンクや差分は最初の要素に届きます。タグ名のように訪問者が保存した値から id を作るときは、レコードのキーと組み合わせて一意にしてください。 - 引数から組み立てる SQL や、
UploadedFile::file_nameがブラウザの送ったまま返すアップロードのファイル名のような、アプリ自身のクエリとファイル。
検査の確かめ方
上の断り方には、それぞれリポジトリにテストがあります。エスケープと属性の検査は HTML ビルダーとスタイルの値のテストに、リクエストは crates/hypersig/tests/axum.rs と upload.rs に、コンパイルエラーは crates/hypersig/tests/ui/fail の trybuild のケースにあります。クレートは Trail of Bits の sharp-edges、insecure-defaults、rust-review、differential-review の Skill で見直し、確かめられた指摘は直したか、アプリが確かめることとして上に挙げたか、その指摘を起こしたリクエストだけが失敗するものです。外部のセキュリティ監査は受けていません。