元記事
今回は、こちらの記事を翻訳してみました。
誤訳等ありましたら、コメント欄にて教えていただけますと幸いです。
本編
PHPを利用されている方々は、現在自分が利用しているPHPバージョンが最新かどうか確認しましょう。
- PHP7.4 -> 7.4.28
- PHP8.0 -> 8.0.16
- PHP8.1 -> 8.1.3
上記、PHP最新バージョンが2022-02-17にリリースされました。
最新バージョンでは、php_filter_float関数が原因のuse-after-free脆弱性1(CVE-2021-21708)を含む、複数のメモリ管理に関するバグが修正されています。
脆弱性検証の結果、この脆弱性を突いた攻撃を受けることで、PHPのプロセスが破壊される可能性があります。
また、DoS攻撃(Denial of Service/サービス拒否)2を受ける可能性もあります。
当然、Mozillaがセキュリティアップデートで、脆弱性について定期的に更新しており、
脆弱性が修正された際には、メモリ管理の不整合についても証拠を示していました。
従って、あなた方はそれらの脆弱性が悪用され、悪意のあるコードが実行されるかもしれないと想定しておくべきでした。
外部から送信されたデータを基に行われるリモートコード実行は、一般的にネットワークへの侵入、情報漏洩、マルウェアの増殖を引き起こし、あなたのコンピューター上のプログラムを破壊するだけでなく、プログラム自体の権限を掌握することも可能です。
無効なバリデーションコード
皮肉なことに、PHPのフィルター関数は外部からのデータを検証するために実装されています。
例えば、ユーザーに数値変換が期待通り行われない可能性のある文字列(例:3.14159
, 3/16 inch
)ではなく、確実に数値(例:5
,7
,11
)を送信してもらうことができます。
今回のCVE-2021-21708は、正しい浮動小数点数(実数・デシマルと呼ばれたりもします。)かどうか検証するコードの一部に原因がありました。
浮動小数点は一般的にドット(国によってはカンマのところもある)を用いて、整数部と小数部を区切ります。
例えば、2.5
は「2と10分の5」または「2と0.5」といったように表すことができます。
PHPの数値フィルター関数は外部から受け取った数値が正しいかだけでなく、2.718283より大きくないことや-1と1の間の数値であることも判定することができます。
もし、受け取った数値が既に浮動小数点数(またはデシマル)である場合、下記に示すコードを利用してデータを検証します。
画像左側が古いPHPバージョン(8.1.2)で右側が新しいPHPバージョン(8.1.3)のコードです。
そのコードにバグはありません。
従って、古いバージョンでも新しいバージョンでも、正しく動作します。
もし、あなたがC言語について知らなかったとしても心配無用です。
ここで覚えておくべきことは、以下の3つの動作が行われているということです。
- エラーのチェックを行う。
- PHPで使用されているメモリを値保持のために解放する。
- 解放したメモリを検証した値に再度割り当てる。
疑問に思ってる方がいらっしゃるかもしれませんが、zval_ptr_dtor()
という関数は、PHPの内部メモリポインターデストラクターの短縮形です。
道を渡ることに例えると、下記の順序です。
- 左右安全確認(エラーチェック)
- 安全確認後、道路横断開始(メモリ解放)
- 安全に横断完了(メモリ再割り当て)
もし、受け取った数値が小数点を含まない整数の場合、バリデーションに使用されるコードが少し異なります。
下記を見ていただけるとわかるように、「エラーチェック→メモリ解放->メモリ再割り当て」の順序が、古いPHPバージョンでは間違っています。
古いPHPバージョンでの順序は、下記の通りです。
- PHPで使用されているメモリを値保持のために解放する。
- エラーのチェックを行う。
- 解放したメモリを検証した値に再度割り当てる。
上記順序では、解放したメモリ領域に本来入るべきでない値やコードが割り当てられてしまいます。
その結果、use-after-free1攻撃に繋がります。
仮にエラーなく検証が完了した場合、検証済の値保持には、解放したメモリ領域ではなく、新しい領域が割り当てられます。
この順序を道を渡ることに例えると、下記のようになります。
- 道路横断開始(メモリ解放)
- 左右安全確認(エラーチェック)<-- 危ない
- 安全に?横断完了(メモリ再割り当て)
PHP8.1.3のコードでは、その順序は正しく修正されています。
しかし、デストラクター5とアロケーター6の呼び出しを一本化する関数(例えば、dtor_and_alloc_in_one_go()
)があれば、より安全なコードになることでしょう。
なぜなら、未来のプログラマーたちが順序を誤ってコードを書き込むことが無くなるからです。
PHP8.1.3のコードの順序は、以下の通りです。
- 左右安全確認(エラーチェック)
- 安全確認後、道路横断開始(メモリ解放)
- 安全に横断完了(メモリ再割り当て)
Visual Studio Codeで作成されたコード差分を見ると、左側(PHP8.1.2)の赤い線で示された処理(メモリ解放)が、右側(PHP8.1.3)では、緑の線下部に移動していることがわかります。
PHP8.1.2(左側・脆弱性あり)
- 道路横断開始(メモリ解放)
- 左右安全確認(エラーチェック)<- 危ない!!!!
- 安全に?横断完了(メモリ再割り当て)
PHP8.1.3 (右側・脆弱性修正済)
- 左右安全確認(エラーチェック)
- 安全確認後、道路横断開始(メモリ解放)
- 安全に横断完了(メモリ再割り当て)
あなたができる事
もし、あなたがPHPを使っている場合
PHPを最新バージョンに更新しましょう。
- PHP7.4 -> 7.4.28
- PHP8.0 -> 8.0.16
- PHP8.1 -> 8.1.3
もし、PHPをあなたの代わりに管理してくれるLinuxディストリビューションを利用している場合は、各ディストリビューションの詳細を確認してください。
もし、あなたがプログラマーの場合
今回のようなわかりづらい脆弱性にいち早く気付くためにも、
C言語で書かれているコードは常にメモリを気にかける必要があることを覚えておいてください。
もし、あなたがプログラマーの場合
コードを書く際には、後発のプログラマーがエラーを誘発する可能性が少ないコーディングを心がけてください。