浅いモデリングによる脆弱性、あるいはCodeIgniter4の自動ルーティングは何が問題だったのか?
この記事は「CodeIgniter 4.1.8 以前に存在する脆弱性」と関連はしますが、たとえ脆弱性が修正されたバージョンを使っていても該当することを記載しています。
この記事で伝えたいこと
- 浅いモデリングとは何か
- 浅いモデリングにより、どのようにCSRF保護が回避される脆弱性が発生するのか
- CodeIgniterでの正しいCSRF対策
浅いモデリングとは
浅いモデリング(shallow modeling)とは、『セキュア・バイ・デザイン』にある用語で、「その場しのぎのモデリング」のことです。 「開発者がうまくモデリングできたと最初に思った時点で、それ以上は深く考えたり疑問を抱いたりすることをやめてしまう」ことです。
このような場合、開発者がどのようにコードを書くのかさえわかれば、設計作業は終了します。言い換えると、コードが動きさえすればOKということで、対象となる概念を正しく捉えてモデリングできているかは問題にはなりません。
浅いモデリングを一言で言い表すなら、「緩い」あるいは「厳密でない」となります。
例えば、ショッピングカートの商品の数量に int 型を使うようなモデリングです。PHPでは、64bit環境では、int は 9,223,372,036,854,775,807(922京)から -9,223,372,036,854,775,808(-922京)までです。冷静に考えれば、いくら何でもそんなに大きな数は必要ないですし、マイナスの整数も不要でしょう。
そして、そのような浅いモデリングは、重要な概念がコードで正しく表現されていないために、バグや脆弱性を作り込んでしまうリスクが高くなります。
この記事では、浅いモデリングがどのように脆弱性を産み出したかの一例を解説します。
CodeIgniter4の自動ルーティング
CodeIgniter4の自動ルーティングとは、URLと実行されるコントローラのメソッドを以下のように対応させる機能のことです。CodeIgniterバージョン1からの機能です。
http://example.jp/コントローラ名/メソッド名[/引数1[/引数2[/...]]]
例えば、URLが http://example.jp/home/index/abc/123
の場合、Home
コントローラの index()
メソッドが、以下のようなイメージで実行されます。
$controller = new Home();
$controller->index('abc', '123');
はい、わかりやすいですね。
URLとコントローラメソッドが1対1に対応しています。何の問題があるのでしょうか?
自動ルーティングの問題点
この自動ルーティングの問題点は大きくは以下の2つです。
- HTTPのリクエストメソッドが考慮されていない
- 1つのコントローラメソッドに対応するURLが複数になる
この記事では (1) のリクエストメソッドが考慮されていない点について見ていきます。
(2) については、先ほど、URLとコントローラメソッドが「1対1」対応と書きましたが嘘です。 正しくは、URLとコントローラメソッドは「多対1」です。この (2) について興味のある方は、 【改訂版】本当は危ないCodeIgniter4の自動ルーティング をご覧ください。
PHPスクリプトとリクエストメソッド
まず最初に、PHPスクリプトとリクエストメソッドの関係を見ておきましょう。
検証のために以下のスクリプトを用意します。
index.php
:
<?php
echo 'REQUEST_METHOD: ' . $_SERVER['REQUEST_METHOD'] . "<br>\n";
echo 'SERVER_SOFTWARE: ' . $_SERVER['SERVER_SOFTWARE'];
上記のスクリプトに対して、 GETリクエストを送信してみましょう。
$ curl http://localhost/index.php
REQUEST_METHOD: GET<br>
SERVER_SOFTWARE: nginx/1.20.1
REQUEST_METHODが GET
となっています。
POSTリクエストを送信してみましょう。
$ curl -X POST http://localhost/index.php
REQUEST_METHOD: POST<br>
SERVER_SOFTWARE: nginx/1.20.1
同じようにスクリプトが実行されています。
違いは、REQUEST_METHODが POST
になったことだけです。
PUTリクエストを送信してみましょう。
$ curl -X PUT http://localhost/index.php
REQUEST_METHOD: PUT<br>
SERVER_SOFTWARE: nginx/1.20.1
同じようにスクリプトが実行されています。
それではOPTIONSリクエストを送信してみましょう。
$ curl -X OPTIONS http://localhost/index.php
REQUEST_METHOD: OPTIONS<br>
SERVER_SOFTWARE: nginx/1.20.1
やっぱり同じようにスクリプトが実行されるだけです。
ちなみにResponseヘッダーを確認しても、OPTIONSリクエストの正当な応答にはなっていません。
$ curl -i -X OPTIONS HTTP/1.1 200 OKdex.php
Server: nginx
Date: Fri, 11 Feb 2022 01:09:14 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: PHP/7.4.23
Expires: Fri, 11 Feb 2022 01:09:14 GMT
Cache-Control: max-age=0
X-Frame-Options: SAMEORIGIN
X-UA-Compatible: IE=edge
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
REQUEST_METHOD: OPTIONS<br>
SERVER_SOFTWARE: nginx/1.20.1
最後にCATメソッドでリクエストを送信してみましょう。
$ curl -X CAT http://localhost/index.php
REQUEST_METHOD: CAT<br>
SERVER_SOFTWARE: nginx/1.20.1
驚くべきことに同じです。HTTPにCATメソッドなどいうメソッドは標準では定義されていませんが。
つまり、PHPはリクエストメソッドがなんであろうと、WebサーバーからリクエストされればPHPスクリプトを実行するだけです。
まあ、これは当たり前のことで、そもそもリクエストメソッドに応じた処理を、開発者がPHPスクリプトに記述する必要があります。
リクエストメソッドに応じた処理を書くことは開発者の責任です。OPTIONSメソッドに対応した処理が index.php
に書かれていないため、OPTIONSリクエストを受けても正しいレスポンスが返らないわけです。
CodeIgniterの自動ルーティング
上記のように、もともとPHPスクリプトを平書きしていた頃から、PHPユーザーはあまりリクエストメソッドを気にしていませんでした。
そもそもGETとPOSTくらいしか使いませんでしたし、$_GET
と $_POST
を区別して使えば問題ありませんでした。
CodeIgniterの自動ルーティングもそんな感覚で設計されたのではないかと想像します。これが浅いモデリングです。
CodeIgniterでも上記の index.php
と同じように、Webサーバーからリクエストされればどんなリクエストメソッドであろうと対応するコントローラのメソッドが実行されます。
しかし、実際には、HTTPにおいてリクエストメソッドは重要な概念であり、モデルから抜けていいようなものではありません。 実際にリクエストメソッドにはそれぞれ意味があり、それに応じた適切な処理をして返す必要があります。
それでは、この自動ルーティングがどのように脆弱性を産み出したかを見ていきましょう。
CSRF保護回避の脆弱性
CSRF保護の仕様
CSRF保護においては、POST、PUT、PATCH、DELETEメソッドをCSRFから保護する必要があるとされています。 これは、それらのメソッドがリソースを書き換えるからです。
逆にGETなどのメソッドはリソースを書き換えない安全なメソッドと定義されており、保護する必要は特にありません。
仮にCSRFによりユーザーが重要なページにGETでアクセスさせられたとしても、何か表示されるだけでそれを見るのもその攻撃されたユーザーだけで、データが書き換えられたり破壊されることはないはずだからです。
従って、CodeIgniter4の実装も、POST、PUT、PATCH、DELETEメソッドの場合にのみ、トークンをチェックしてCSRFから保護するようになっています。
つまり、GETメソッドなどの上記以外のリクエストは、機能を有効にしたつもりでも、実際には全く保護されないということです。
しかし、安全なメソッドでリクエストされた際に何をするかは、PHPを書く開発者の責任です。GETリクエストなら安全なのではなく、GETリクエストに対しては安全な処理をするように開発者がコードを書かないといけないわけです。
次に、このCSRF保護が回避されてしまう脆弱性を産み出しす 2つの材料を見ていきましょう。
コントローラロジックの分離
もともとCSRF対策において、保護すべきなのは、重要なデータ加工処理であり、それを実行するコントローラメソッドです。
ですから、当初は、コントローラのメソッドにCSRF保護のロジックを記述していました。
CodeIgniter v1でのコード例
:
$this->ticket = $this->session->userdata('ticket');
if (
! $this->input->post('ticket')
|| $this->input->post('ticket') !== $this->ticket
) {
echo 'クッキーを有効にしてください。クッキーが有効な場合は、不正な操作がおこなわれました。';
exit;
}
重要な処理を特定し、そのメソッドの最初でCSRF対策のトークンをチェックするのです。これなら、チェックと重要な処理がくっついており問題ありませんでした。
しかし、時代が進むにつれ、コントローラの肥大化が問題となり、コントローラのロジックがどんどん分離されるようになりました。
CSRF対策もその1つであり、具体的には、CodeIgniter4ではコントローラフィルターという機能に分離されています。つまり、別のクラスにロジックは移動してしまい、コントローラのメソッドにはCSRF対策のコードは影も形もありません。
CodeIgniter4のCSRF保護を有効にするためには、フィルターの設定ファイルで設定するか、ルーティングの設定でそのルートに対するフィルターを設定する必要があります。
フィルターの設定例
:
public $globals = [
'before' => [
'csrf',
],
'after' => [],
];
この結果、コントローラ内の重要な処理の直前にあったCSRF保護のコードは、コントローラから完全に分離され、設定によってコントローラに対応づけられるという弱い対応関係になりました。
コントローラ処理の抽象化
もう 1つ、これはかなり以前から行われていたかも知れませんが、GETでもPOSTでもどちらのリクエストが来ても同じコントローラのコードで処理できるように記述するというコードの書き方があります。
もともと、PHPには $_REQUEST
変数がありますし、$_POST
を検索し、なければ $_GET
から値を取得するというメソッドがフレームワークで提供されていることもあります。
GET/POSTだけでなく、JSONによるPUTなどからも同一のメソッドでデータを取得できたりします。こういうメソッドを使えば、1つのコントローラメソッドでいろいろなリクエストメソッドのリクエストを処理できます。便利ですね。
CSRF保護の回避方法
さて、脆弱性を産み出しす 2つの材料がそろいましたので、具体的な回避方法に進みます。
CSRF保護機能の仕様により、この保護を回避するためには、リクエストメソッドをGETなどチェックされないものにすればいいだけです。
つまり、通常POSTで送信されるべきURLに対して、CSRFでユーザーにGETリクエストを送信させればチェックは実行されません。
ここで、そのGETリクエストを受けたコントローラメソッドがGETの場合でもPOSTと同じように処理を実行できる抽象化されたコードになっていれば、CSRFによる攻撃が成功します。そして、自動ルーティング機能は、GETでもPOSTでもリクエスト可能にしてくれます。
つまり、攻撃方法は以下のようになります。
- 罠サイトを作り、ユーザーを誘導し、重要な処理をするURLへGETなど保護されないメソッドでリクエストを送信させる
そのURLが開発者の想定外のリクエストメソッドでも処理を実行可能なら、CSRFによる攻撃が成功します。
これは何の脆弱性か?
これはアプリケーションの脆弱性でありフレームワークの脆弱性ではありません。
なぜなら、CSRF保護機能がGETリクエストを保護しないのは仕様であり、自動ルーティングがGETリクエストを受け付けることも仕様だからです。
言い換えると、CSRF保護対象の重要な処理が、保護されるリクエストメソッドでのみリクエスト可能にするのは開発者の責任ということになります。
つまり、開発者は、もともと以下のいずれかをする必要があったのです。
- そのコントローラメソッドにPOST/PUT/PATCH/DELETE以外のリクエストが絶対に来ないように設定する
- POST/PUT/PATCH/DELETEメソッド以外のメソッドでリクエストが来た場合は重要な処理を実行しないようにする
自動ルーティングを使っている限り、1 は不可能なので、必然的に 2 を行う必要があります。
実際に、以下のようなコードをコントローラメソッドの先頭に記載していれば、CSRFによる攻撃は成功しません。
if (strtolower($this->request->getMethod()) !== 'post') {
return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
}
また、例えば、POSTでの処理を前提としているコントローラメソッドで、$_POST
や $_POST
の値を取得するCodeIgniter4の $this->reqest->getPost()
メソッドを使って、開発者が想定するHTTPメソッドの場合のみデータを取得できるようなコードを書いていれば、GETリクエストが送信された場合には、必要なデータが取得できず重要な処理が検証エラーで実行されず、CSRFによる攻撃は成功しません。
メンタルモデルと現実の解離
現実には、HTTPにおいて、リクエストメソッドは非常に重要な概念であるにも関わらず、自動ルーティングにはその概念が表現されていませんでした。 現実と実装が解離しています。
その結果、開発者のメンタルモデルからもリクエストメソッドが抜け落ちることを助長します。
開発者はコントローラのコードを書く際に、リクエストメソッドに対応した適切な処理を記述しなければならないにも関わらす、リクエストメソッドを意識せずにコードを書いてしまいます。また、抽象化した再利用しやすい汎用的なコードを書いてしまい、結果として脆弱性を産み出します。
あるいは、CSRF保護はフレームワークが全部やってくれると思っており、何も考えていないということもあるかも知れません。
このような経緯で、脆弱性が生まれやすい環境ができました。
例えば、 リクエストが GET http://example.jp/home/index/abc/123
の場合、Home
コントローラの getIndex()
メソッドを実行するという仕様だったら、リクエストメソッドが絶えず意識されたことでしょう。
CodeIgniterでの正しいCSRF対策
CodeIgniterでの正しいCSRF対策は以下のいずれかを必ず実装することです。
- そのコントローラメソッドが、想定するリクエストメソッド(通常はPOSTまたはPUT/PATCH/DELETE)のリクエスト以外では絶対に実行されないように設定する
- 想定するリクエストメソッド以外のメソッドでリクエストが来た場合は重要な処理を実行しないようにする
CodeIgniter4では、1 は自動ルーティングをオフに設定し、リクエストメソッドに対応するルートを全て手動設定すれば可能です。
具体的には、$routes->add()
メソッドを使わずに、$routes->get()
や $routes->post()
でルートを設定します。
そのように全ルートを手動で設定していれば、コントローラのメソッドが想定しているリクエストメソッドと実際にルーティングされるリクエストメソッドが解離する可能性は極めて低いです。
POSTで受けるつもりのコードを書いていて、仮に誤って $routes->get()
でルートを設定してしまったとしても、正常系の処理がうまく動作せず、容易に気がつくからです。
逆に自動ルーティングでは、POSTで受けるつもりのコードを書いていても、自動的にGETリクエストでもアクセス可能です。 POSTするHTMLフォームを作成し、処理がうまく動作すればOKだと思うでしょう。念のためGETに変えて処理されないかテストする開発者はほぼいないと思われます。
CodeIgniter3では、1 は機能がなく無理なので、2 を実装します。なお、CodeIgniter3のCSRF保護はPOSTメソッドしか保護しません。
まとめ
- コードが動きさえすればOKというその場しのぎのモデリングを「浅いモデリング」と言います。
- 浅いモデリングでは脆弱性を作り込むリスクが高くなります。
- CodeIgniterの「自動ルーティング」は浅いモデリングでした。
- 自動ルーティングには重要概念であるリクエストメソッドが欠落しており、その結果、CSRF保護が回避される脆弱性が作られやすい状況を助長しました。
- CodeIgniter4では自動ルーティングはオフに設定し、
$routes->add()
も使わないようにしましょう。 - CSRF保護対象となるコントローラメソッドでは、リクエストメソッドが想定されたものか確認しましょう。
(2022-08-15 追記) CodeIgniter 4.2からは、より安全な「自動ルーティング(改善)」が実装されました。
参考
Date: 2022/02/28