PHX セキュリティ監査報告 (v0.2)
この報告は 3 回の監査をまとめたもの。§1〜§6 が 1 回目(と残存リスクの解消)、§7 が 2 回目の脆弱性診断、
§8 が 2 回目で対象外にした部分(phx lsp、phx migrate、バイトコードキャッシュ)の診断。
実施日: 2026-11-02 対象: PHX 処理系のすべてのクレート(構文解析・型検査・コンパイラ・VM・JIT・標準ライブラリ・Web サーバー・CLI ツール)
1. 方法
- 独立した 3 つのレビュー。実装者の前提に引きずられないよう、担当を分けた 3 人のレビュアーがソースを読み、
実際に攻撃コード(PoC)を動かして確かめた。
- A: HTTP 層(ルーティング、静的ファイル、セッション、CSRF、multipart、ヘッダー)
- B: ランタイム(VM、JIT、メモリ・時間の上限、標準ライブラリ、ファイル権限、SQLite)
- C: 言語の保証とツール(エスケープ、型システムの健全性、
Html/Sql型、モジュール読み込み、phx install/migrate/lsp)
- 修正と再検証。すべての指摘を修正し、レビュアーが残した PoC を修正後のビルドでもう一度実行した。
- 自動テスト。ゴールデンテストに回帰テスト(
tests/cases/err_security.phx、security.phx)を追加した。 ほかに、実サーバーへの HTTP 結合テストと、変異させたプログラムによるファジング(ローカルで約 22,000 件)を CI で常に実行する。
2. 結果の概要
| 重大度 | 件数 | 対応 |
|---|---|---|
| Critical | 3 | すべて修正 |
| High | 9 | すべて修正 |
| Medium | 10 | すべて修正 |
| Low / Info | 18 | すべて修正(2 件は最初は受容とし、§4 の追加対応で修正) |
| 追加対応で見つかったもの | 1 | 修正(§4 の N1、Medium) |
Critical と High はすべて修正し、PoC で再現しないことを確認した。最初の報告で「残っているリスク」とした項目も、 §4 の追加対応で、仕組みとして受け入れているもの(§5)を除いて解消した。
3. 指摘と対応
Critical
| # | 内容 | 対応 | 確認 |
|---|---|---|---|
| C1 | Windows で /INDEX.PHX、/index.phx::$DATA、8.3 短縮名 (/DASHBO~1.PHX、/ENV~1) のような別名で、.phx のソース・テンプレート・ドットファイルがそのまま配信された |
正規化したパスの各要素が、要求されたパスと完全に同じ綴りのときだけ配信する。: を含むパスは拒否。.phx / .phx.html / . で始まるファイルは静的ファイルとして扱わない |
実サーバーで全パターン 404、単体テストに追加 |
| C2 | 30 万段入れ子の list を捨てると再帰的な解放でスタックが溢れ、サーバープロセスごと落ちた(catch_unwind では止められない) |
Value の解放と == の比較を反復処理にした |
100 万段で正常終了(テスト deep_values_do_not_overflow_the_stack) |
| C3 | "a"->padStart(10^13, "b") のような巨大確保の失敗でプロセスが abort した |
出力が入力より大きくなり得る組み込み関数は、確保する前に reserve() で上限を確認する(pad、repeat、replace、chars、split、join、concat、range、ファイル読み込み、文字列連結) |
捕捉できない致命的エラー(メモリ上限)になり、プロセスは生きている |
High
| # | 内容 | 対応 | 確認 |
|---|---|---|---|
| H1 | ジェネリックの型引数を is で絞り込むと never になり、任意の値を Sql や Html として通せた(SQL インジェクション、raw() を使わない XSS) |
型引数・any の絞り込みは検査対象の型にする。interface や Sql を考慮し、結果が never になる絞り込みは適用しない。Html の連結は実行時にも素の文字列をエスケープする |
PoC はコンパイルエラーになる |
| H2 | && / || の右辺や render() の中で呼び出しがあっても、プロパティの絞り込みが残った |
呼び出しのたびに世代番号を進め、呼び出しをまたいだプロパティの事実は捨てる | PoC 4 件ともコンパイルエラー |
| H3 | public/ の .php などが生のまま配信された |
静的ファイルは既知の拡張子(css、js、画像、フォント など)だけを配信する | /config.php は 404 |
| H4 | 匿名の GET で CSRF トークンのためにサーバー側セッションが作られ、10 万件で満杯になり、新しい訪問者がログインやフォーム送信をできなくなった | CSRF トークンを「ブラウザごとのランダムな Cookie の HMAC」にして、サーバー側に状態を持たない。セッション保存領域は件数とバイト数の上限付きで、古いものから追い出す | 単体テスト(上限を超えても新しい訪問者がセッションを得られる) |
| H5 | ヘッダー・本文の受信にタイムアウトがなく(slowloris)、接続数の上限もなかった | ヘッダー 10 秒(keep-alive の待ち時間も含む)、本文 30 秒、同時接続 1024 | 途中までのヘッダーは 10 秒で切断 |
| H6 | メモリ上限が組み込み関数の中ではほとんど効いていなかった | 組み込み関数の呼び出しのたびに確認、確保前の reserve()、SQLite の文字列長・ヒープ上限 |
16 MB の設定で 1 GB 確保の PoC が止まる |
| H7 | SQLite のクエリが実行時間の上限を無視した | SQLite の progress handler で上限時刻に中断する | 無限の再帰 CTE が 2 秒で止まる |
| H8 | 実行時間の上限が命令数で数えられていて、遅い組み込み関数を繰り返すと何倍も延びた | 組み込み関数の呼び出しのたびに時刻を確認する | |
| H9 | 書き込み可能なディレクトリに置かれた「リンク先が存在しない」シンボリックリンク経由で、サンドボックスの外にファイルを書けた | シンボリックリンクとリパースポイント(ジャンクション)を拒否し、解決済みのパスで操作する | リンクもジャンクションも拒否 |
Medium
| # | 内容 | 対応 |
|---|---|---|
| M1 | エスケープが文脈を見ていなかった(引用符なしの属性、href="javascript:..."、<script>、<style>、イベント属性) |
<?= ?> の位置の HTML 文脈を構文解析時に判定し、安全でない場所への文字列の挿入をコンパイルエラーにする。safe_url() と json_html() を追加 |
| M2 | response()->type("image/svg+xml") や "text/plain, text/html" でエスケープが外れた |
エスケープを外すのは許可リスト(text/plain、application/json、text/csv など)の型だけ。カンマを含む Content-Type は拒否 |
| M3 | render() が .env などプロジェクト内の任意のファイルを読めた。ジャンクションでプロジェクトの外も読めた |
テンプレートは .phx.html だけ。正規化したパスがプロジェクトかパッケージの中にあることを確認する |
| M4 | phx install が既存の vendor/<名前>/.git の中で git を実行し、同梱された hook が動いた |
毎回一時ディレクトリに新しく clone し、hook・fsmonitor を無効にし、プロトコルを許可リストで制限する。lock のコミットは 40 桁の 16 進数だけを受け付ける |
| M5 | phx migrate が require 'auth.php' をコメントにして「型検査に通る」と表示した(認証チェックが黙って消える) |
コンパイルできない TODO_migrate(...) 呼び出しにする。既存ファイルの上書きには --force が必要 |
| M6 | スクリプトが Content-Length などを設定でき、keep-alive のレスポンスがずれた(レスポンス分割) |
通信の枠組みに関わるヘッダーは組み込み関数とサーバーの両方で拒否する |
| M7 | 開発モード(エラー詳細の表示)が既定だった | 既定で開発モードになるのはループバックで待ち受けるときだけ。--dev / --prod で明示できる |
| M8 | SQLite の PRAGMA temp_store_directory などでプロセス全体の設定を変えたり、ほかのディレクトリに書いたりできた |
authorizer で ATTACH/DETACH と危険な pragma を拒否する |
| M9 | 範囲外の slice() で内部パニックが起きた |
飽和演算にした。リリースビルドでも整数オーバーフローを検査する |
| M10 | アップロードを .phx として保存したり、write = ["."] でソースを書き換えたりできた |
.phx は書き込めない。public/ 配下やプロジェクトのルートは [permissions].write に指定できない |
Low / Info(主なもの)
- アップロードのファイル名:
:、Windows の予約名、末尾のドットを除去する(修正)。 - セッション: ログイン(
regenerate())で CSRF の Cookie も更新、24 時間の絶対有効期限、HTTPS では__Host-接頭辞と HSTS。 セッションのあるレスポンスにはCache-Control: private, no-storeを付ける(修正)。 - リダイレクト:
redirect()はサイト内のパスだけ。外部へはredirectExternal()(オープンリダイレクト対策)。 - パスの依存パッケージ: 絶対パスと UNC パスは指定できない。開いただけで SMB 接続が起きるのを防ぐ(修正)。
- bytecode キャッシュ: JIT は読み込んだバイトコードのレジスタ・ジャンプ・呼び出しの範囲を検証する(修正)。 キャッシュ自体はソースコードと同じ信頼レベルとして扱う(仕様として受容)。
password_verify: 保存されたハッシュのコスト値に上限を設けた(修正)。- その他:
_で始まるファイルはページとして公開しない。ステータス 1xx は拒否する。JSON の\uを厳密に解釈する。 LSP のヘッダー長を制限し、UNC の URI を拒否する。いずれも修正済み。
4. 追加の対応(残っていたリスクの解消)
最初の報告で受容した項目を、あらためて修正した。
| # | 以前のリスク | 対応 | 確認 |
|---|---|---|---|
| R1 | 書き込み可能なディレクトリに外部ファイルへのハードリンクが置かれていると、そこを読み書きできた | 開いたファイルのハンドルでリンク数を調べ、1 本でなければ拒否する(読み書きとも)。通常のファイル以外(ディレクトリ、FIFO、デバイス)も拒否 | 実ファイルへのハードリンク経由の読み書きが PermissionError になり、外のファイルは変わらない |
| R2 | パスの検査からファイルを開くまでの間に、ディレクトリをリンクにすり替えられた(TOCTOU) | 新しい safefs モジュール。許可したディレクトリのハンドルから 1 段ずつ、リンクを拒否しながら開く。Unix は openat + O_NOFOLLOW、Windows は FILE_FLAG_OPEN_REPARSE_POINT で開き、各ディレクトリを FILE_SHARE_DELETE なしで開いたままにして、使用中の改名・削除・すり替えを防ぐ。書き込みは切り詰めずに開いて検査してから切り詰める |
Windows: ジャンクション・シンボリックリンク経由をすべて拒否。ハンドルを持っている間はディレクトリを改名できないことを単体テストで確認。Linux(Docker): シンボリックリンク・FIFO・o+w ディレクトリの拒否をテスト |
| R2' | SQLite はファイルを名前で開くため、R2 の方法をそのまま使えない | ディレクトリを開いたまま固定し、データベースと -journal / -wal / -shm が「存在しないか、リンクのない通常のファイル」であることを開く前後に確認。SQLITE_OPEN_NOFOLLOW を付ける。Unix では、ほかのユーザーが書き込めるディレクトリ(o+w)を拒否する |
ジャンクション経由の Database::open が拒否される |
| N1 | (今回見つかった、Medium)file_put_contents("data/a.phx::$DATA", ...) や "data/b.phx." で、.phx の書き込み禁止をすり抜けて a.phx を作れた。CON などのデバイス名も使えた |
パスの各要素について、:、末尾のドット・空白、CON NUL COM1 LPT1 などのデバイス名、制御文字を拒否する(すべての OS で同じ規則) |
3 つの PoC とも PermissionError |
| R3 | CSP が既定でオフだった | 既定で有効にした。テンプレートに書いた <script> タグには、構文解析の時点で位置を記録しておき、レスポンスごとの nonce を自動で付ける。raw() などで注入された <script> には付かないので動かない |
HTTP 結合テストで、ヘッダーの nonce とテンプレートの 2 つの <script> が一致し、raw() の <script> には付かないことを確認。サンプルサイトはブラウザで動作 |
| R4 | レート制限がなかった | rate_limit($key, $max, $seconds) / rate_limit_reset($key) と request()->ip を追加。サイト全体の上限 [security] requests_per_minute(超えると 429 と Retry-After)。カウンター表は 10 万キーまでで、古い時間枠から捨てる。サンプルのゲストブックで使用 |
単体テストと HTTP 結合テスト |
| R5 | セッションがメモリ内だけだった | [security] session_store = "dir/" でファイルに保存。ファイル名は ID の SHA-256(一覧から ID が分からない)、書き込みは一時ファイルからの rename で原子的、読み書きは safefs 経由。件数・容量の上限と期限切れの掃除あり |
別のサーバープロセスから同じセッションを読めることを HTTP 結合テストで確認 |
5. 残っているリスク
| 項目 | 内容 | 判断 |
|---|---|---|
| 文脈判定は近似 | <?= ?> の文脈判定はソースの出現順に基づく。Html 値を連結して動的に組み立てた HTML の中までは解析しない |
Html は「信頼できる HTML」を表す型で、作れるのは raw()・escape()・テンプレートだけ。raw() の中身は利用者の責任として仕様に明記。既定の CSP が、注入されたスクリプトの実行を二重に防ぐ |
| SQLite の補助ファイル(Windows) | 開く前後の検査の間に、ジャーナルファイルの名前でシンボリックリンクを作られる可能性がごくわずかに残る | Windows でファイルのシンボリックリンクを作るには管理者権限か開発者モードが必要で、ディレクトリ自体はすり替えられない。権限ディレクトリはスクリプト専用にする |
| レート制限はプロセスごと | 複数のサーバープロセスでカウンターを共有しない | 今後の課題。リバースプロキシの後ろでは request()->ip がプロキシのアドレスになる点を仕様に明記 |
6. 継続的な確認
cargo test --workspace: ゴールデンテスト(インタプリタと JIT の両方)、セキュリティの回帰テスト、 実サーバーへの HTTP 結合テスト(別名・トラバーサル・CSRF・Cookie 属性・時間上限)、ファジング。- CI: Linux / Windows / macOS、
clippy -D warnings、長時間のファジング、cargo audit(依存クレートの既知脆弱性)。 - Unix 向けのコード(
safefsのopenat経路)は、Linux の Docker コンテナでも全テストと clippy を実行して確認した。 unsafeを使うのは、メモリ使用量を数えるアロケータ(phx_vm/src/alloc.rs)、 JIT が生成したコードの呼び出し(phx_jit、Vm::call_jit)、OS のファイル API の呼び出し (phx_std/src/safefs.rs)の 3 か所だけ。
7. 2 回目の脆弱性診断
実施日: 2026-11-02(1 回目の修正と残存リスクの解消の後)
方法
1 回目の結論に引きずられないよう、新しいレビュアー 4 人にソースだけを渡し、それぞれ実際に攻撃して確かめた。
- A: 1 回目の後に書いたコード(
safefs、ファイル保存のセッション、レート制限、CSP の nonce) - B: Web サーバーとサンプルサイト(外からの攻撃)
- C: テンプレートの文脈判定と型システム
- D: SQL、実行時の上限、JIT、バイトコードキャッシュ、ツール
ほかに、依存クレートを cargo audit で調べた(既知の脆弱性 0 件。保守されていない atomic-polyfill を
postcard の既定機能を外して取り除いた)。unsafe のコードと CI の設定も読んだ。修正の確認では、
レビュアーが残した PoC を修正後のビルドでもう一度実行し、回帰テストに加えた。Linux(Docker)でも全テストを実行した。
結果の概要
| 重大度 | 件数 | 対応 |
|---|---|---|
| High | 6 | すべて修正 |
| Medium | 9 | すべて修正 |
| Low / Info | 11 | 9 件を修正、2 件はドキュメントで明記 |
High
| # | 内容 | 対応 |
|---|---|---|
| R2-1 | escape()・htmlspecialchars()・nl2br() の結果(Html)を href の先頭や <script> の中に置けた。HTML エスケープでは javascript:alert(1) もスクリプトのコードも止まらず、テンプレートの <script> には nonce も付くので CSP でも防げなかった |
文脈ごとに安全と確かめた型 SafeUrl(safe_url())と SafeJs(json_html())を追加。URL の先頭には SafeUrl と数値、<script> の中には SafeJs と数値だけを許す。json_html() の値は属性値の中では拒否(" で属性が終わるため) |
| R2-2 | テンプレートの HTML の間に書いた echo(<?phx echo $x; ?>)は文脈の検査を受けなかった |
echo にも <?= ?> と同じ検査をする |
| R2-3 | file_put_contents("views/page.phx.html/", ...)(末尾の /、/.、\)で .phx の書き込み禁止をすり抜け、テンプレートを書き換えてコードを実行できた。unlink と Database::open は禁止を確認していなかった |
パスを正規化した後の最終的なファイル名で確認し、書き込み・削除・保存・データベースのすべてに適用。末尾が / \ /. のパスは拒否 |
| R2-4 | Windows の 8.3 短縮名(views/GREETI~1.HTM)で .phx.html のテンプレートを書き換えられた |
開いたハンドルから本当の名前を取り出し、要求した名前と違えば(短縮名などの別名)拒否する。ディレクトリにも適用 |
| R2-5 | join()、json_encode()、json_html()、dump()、echo、文字列連結が確保前にメモリ上限を確認せず、1 回の呼び出しで数 GB を確保してプロセスごと落ちた(serve も停止) |
結果の大きさを先に計算して reserve() する(join)、出力が 64 KB を超えたら上限と時間を確認しながら進める(JSON・dump)、echo と連結はエスケープで 6 倍になる分を含めて確保前に確認する |
| R2-6 | 共有された構造($a = [$a, $a] を 60 回)どうしの == が指数時間かかり、時間制限も効かず、ワーカーを占有した |
比較済みのリストとマップの組を覚えて 1 回だけ比べる(線形時間)。unique() のような二乗の処理は時間制限を確認しながら進める |
Medium
| # | 内容 | 対応 |
|---|---|---|
| R2-7 | テンプレートの文脈判定がブラウザと違った: </scripts> を終了タグとみなす、<script> 内の <!--<script>、<!-->・--!>、<textarea>・<title> の中身、<svg> の中、srcdoc、href=" ..." や href="j<?= ?>" のように先頭の判定を文字でずらす方法 |
WHATWG の字句解析に合わせて書き直した(終了タグの区切り、script の escaped / double escaped 状態、コメントと bogus コメント、RCDATA / RAWTEXT 要素、外部コンテンツ)。URL は、前に書いた文字列に / ? # : が出るまで先頭とみなす。javascript: の URL と srcdoc は文字列を拒否 |
| R2-8 | 関数やループの中で <script> を開いて閉じないと、呼び出し順で文脈がずれた |
ブロックと関数の本体は、始まったときと同じ HTML の状態で終わらなければならない(関数は空の文書から始める) |
| R2-9 | <script<?= $x ?>> でタグ名の途中に挿入でき、攻撃者の src= に nonce が付いた(CSP の回避) |
タグ名の途中はタグの中とみなしてエラーにする |
| R2-10 | 参照が循環するオブジェクト($n->next = $n)がリクエストの後も解放されず、サーバーのメモリが増え続けた |
VM が作ったオブジェクトを弱参照で覚え、実行の終わりにフィールドを空にして循環を切る |
| R2-11 | 深い入れ子のソース(5 万段の 1+1+...、if、[[[...]]] など)で構文解析・型検査・コンパイルのスタックがあふれた(捕捉できないので serve も停止)。エラー表示は 1 行 5 万文字を毎回引用し、出力が 8.5 GB になった |
入れ子は 500 段まで(超えたら 1 つのエラーで解析をやめる)。エラーは 50 件まで、長い行はエラーの周りだけを表示 |
| R2-12 | redirect("/<TAB>/evil.com") がサイト内のパスとして通り、ブラウザはタブを取り除いて //evil.com へ移動した(オープンリダイレクト) |
制御文字・空白・\ を含むパスを拒否。redirectExternal() も制御文字を拒否 |
| R2-13 | session_store = "." や "data/" を指定でき、期限切れの掃除がプロジェクトのファイルを消し、.gitignore を * で上書きした。スクリプトの権限ディレクトリと重なるとセッションを偽造・閲覧できた |
セッション用の専用ディレクトリだけを許す(ルート、public/、[permissions] と重なるものは不可)。掃除はセッションファイルの名前の形のものだけを消す。.gitignore はないときだけ作る |
| R2-14 | ログアウトや regenerate() の直後に、並行して動いていたリクエストが保存するとセッションが復活した |
保存時に元のセッションが消えていれば保存しない(メモリ・ファイルとも) |
| R2-15 | SQLite のメモリがスクリプトの上限に数えられず、:memory: のデータベースや cache_size で 1 リクエストが数百 MB を使えた |
接続ごとにページキャッシュを 2 MB にし、:memory: は上限に合わせて max_page_count を設定。cache_size・temp_store・max_page_count の pragma は拒否 |
Low / Info
- レート制限(修正): 10 万個のキーで制限中のキーを追い出せた → 上限に達していないキーから捨てる。同じキーで時間枠を変えると数え直しになった → 時間枠ごとに数える。IPv6 は /64 ごとにまとめる。
- 設定(修正):
write = ["public."]や["..."]が Windows でpublic/やルートを指した → パスの各要素にファイル名と同じ検査をする。巨大なsession_ttlでオーバーフロー → 範囲を検査。 - 型パラメータが 5 個以上(修正): 型検査がパニックした → 範囲外は未束縛として扱う。
mktime/checkdate(修正): 範囲外の値が黙って折り返された →ValueError。password_verify(修正): 保存されたハッシュのコストの上限を 64 MiB・8 回・並列 4 に下げた。- セッションファイル(修正): Unix で他のユーザーから読めた → 0600 で作る。30 秒ごとの掃除の間に大量に書かれたとき → 書き込み量で早めに掃除する。
phx fmt/phx test(修正): ディレクトリをたどるときにシンボリックリンク・ジャンクションをたどり、ループで止まらない、外のファイルを書き換える可能性があった → リンクはたどらない。- CI(修正): 第三者の GitHub Actions をタグで指定していた → GitHub 自身のアクションと rustup・
cargo auditだけにし、checkout の認証情報を残さない。 - CSP の nonce(修正):
<textarea>の中の<script文字列にも nonce が入った → RCDATA 要素の中は対象外。ドキュメントの'nonce-{nonce}'の書き方でも動くようにした。 - エスケープを外す Content-Type(明記): 仕様に書いていない型(
text/markdownなど)があった。いずれもブラウザが HTML として表示しないので、仕様のほうを実装に合わせた。 - CSRF(明記): Cookie の HMAC 方式なので、Cookie を書き換えられないことが前提。本番は HTTPS と
secure_cookies = trueを使うよう仕様に明記。
問題がなかったもの(主なもの)
パストラバーサルとソースの露出(エンコード、大文字小文字、8.3、ADS、末尾のドット)、レスポンス分割、CSRF の各迂回、
Cookie の属性、アップロード、本番モードのエラー表示、slowloris・巨大な multipart・深い JSON、sql"..." の
パラメータ化(型システムで Sql を作る方法は見つからなかった)、SQLite の authorizer、JIT とインタプリタの結果の一致と
上限、phx install の git 呼び出し(-- の区切り、リビジョンの文字種、プロトコルの許可リスト)。
今回見ていないもの
phx lsp、phx migrate、バイトコードキャッシュは対象外にした(§8 で診断)。
8. 3 回目の診断(lsp・migrate・キャッシュ)
実施日: 2026-11-02。2 回目で対象外にした 3 つを、新しいレビュアー 1 人が PoC で確かめた。あわせて、2 回目の
残りだった new Database(整数) による接続ハンドルの偽造を塞いだ。
| # | 重大度 | 内容 | 対応 |
|---|---|---|---|
| R3-1 | High | キャッシュはソースと同じ信頼レベル、という前提が成り立っていなかった。エントリはどのスクリプト用かを確かめず(依存リストが空なら常に有効)、stamp(バージョン・本体のサイズと時刻)も公開情報なので、ほかの人から受け取ったプロジェクトの .phx-cache/ に、ソースと違うバイトコードを入れておける(読んだソースとは別のコードが動く) |
ユーザーごとの秘密鍵(プロジェクトの外、Unix は 0600)による HMAC で署名し、合わないものは読まない。エントリにスクリプトとルートの正規パスを記録して照合し、スクリプト自身が依存に含まれることを確かめる |
| R3-2 | Medium | 依存ファイルを更新時刻だけで比べていたため、時刻を保つコピー(cp -p、rsync -t、展開)や同じ長さの書き換えで古いバイトコードが動き続けた(セキュリティ修正を配置しても効かない)。読み込みと時刻の取得の間に書き換わる競合もあった |
依存ファイルごとに、コンパイルした内容の SHA-256・サイズ・時刻を記録する。キャッシュを読むときは内容を必ず確かめ、サーバーのメモリ内の確認でも、記録の直前に変更されたファイル(時刻の分解能の内側)は内容を確かめる |
| R3-3 | Medium | キャッシュの書き込みがリンクをたどり、.phx-cache をジャンクションにしたプロジェクトや .gitignore のシンボリックリンクで、プロジェクトの外のファイルを上書きできた。一時ファイルの名前も予測できた |
safefs で読み書きし、リンク・ハードリンクを拒否する。.phx-cache 自体がリンクなら使わない。一時ファイルは乱数の名前、.gitignore はないときだけ作る |
| R3-4 | Low | [permissions].write に .phx-cache/ や vendor/ を指定でき、スクリプトがコードとして読み込まれるファイルを書けた |
. で始まるディレクトリ、vendor/、パッケージのディレクトリを write に指定できない |
| R3-5 | Medium | phx migrate が <?PHP / <?Php(大文字)を変換せず、認証の require と exit を含む PHP がただの文字列として残り、「型検査に通る」と表示した(認証チェックが黙って消える) |
開始タグを大文字小文字を区別せずに変換する。変換できない <? は、コンパイルできない TODO_migrate(...) を付けて残す |
| R3-6 | Medium | phx migrate の出力が、リンク先のない(dangling)シンボリックリンクをたどって任意の場所に書かれた。self.PHX のように綴りを変えると入力ファイル自体を上書きできた |
safefs で書き、リンクやハードリンクには書かない。入力と出力は正規化したパスで比べる |
| R3-7 | Low | phx migrate が、コメントや文字列の近くに日本語などの非 ASCII 文字があるとパニックした |
比較をバイト単位にした |
| R3-8 | Low | phx lsp の URI の検査を、デコード後の UNC パス(file:///%5C%5Chost%5Cshare)や \\?\・\\.\ で回避でき、開いていないファイルを読み、SMB 接続を起こし、名前付きパイプを指定すると応答しなくなった |
デコード後にドライブ文字で始まるローカルパスだけを受け付け、デバイス名と .. を拒否する。ホバーは開いている文書だけ。開ける文書は 1000 個まで。不正な JSON にはエラーを返して続ける |
| R3-9 | Info | new Database(0) で接続ハンドルを偽造できた(同じリクエストの中だけ) |
private / protected のコンストラクタを new で検査するようにし(これまで検査していなかった)、Database のコンストラクタを private にした |
問題がなかったもの
phx lsp はファイルを書かない。本文 64 MiB・ヘッダー行 8 KiB の上限、深い入れ子への耐性(構文解析の 500 段の
上限)。phx migrate は require・exit・eval・$$var・extract・スーパーグローバルなどを、
コンパイルできないコードにして残す。キャッシュの形式(postcard)の復号は安全で、敵対的なバイトコードでも
インタプリタは境界検査のパニックで止まり(catch_unwind で捕捉)、JIT は well_formed の検査で拒否する。
確認
修正後のビルドでレビュアーの PoC をすべて再実行し、再現しないことを確かめた。キャッシュの偽造・再利用・
同じ長さの書き換え・ジャンクション、コンストラクタの可視性、lsp の URI、設定の検査を回帰テストに加えた。