セキュリティ・コンサルティング Sucuri の 2014年11月の記事 によると、WordPress プラグインの3大脆弱性は、SQL インジェクション(SQLi)、クロス・サイト・スクリプティング(XSS)、ファイル・インクルージョン(FI)とのことです。
冒頭のグラフ は最新の分析結果で、この事実を裏付けています。プラグイン開発の チュートリアル や 手引き には、脆弱性を作り込まないためのポイントが上手にまとめられているのに、なぜ無くならないのでしょうか?
「ヒトが作るものだから、バグがあっても当然」と言ってしまえばそれまでですが、Sucuri のブログ を読み漁っていると、こと脆弱性に関する限り、WordPress に特有の「思い込み」や「見過ごし」といった、ヒトの心理的・認知的な盲点が原因の多くを占めているんじゃないかと思えてきます。
だから、単に PHP のセキュリティ How To ではなく、私が公開している唯一のプラグイン の機能の設計と脆弱性を作り込まない実装のために勉強したたこと − 実際に攻撃を試して脆弱性を追跡したからこそ見えてきたこと − を中心にまとめてみたいと思います。ただし具体的な攻撃方法は、元記事以上に詳しくは触れません、あしからず 。
SQL インジェクション
端的には、OWASP の SQL インジェクション チートシート に示されている「防御の3原則+2」が基本なんだと思います。
OWASP の SQL インジェクション チートシート
優先すべき防御策:
- オプション #1: プリペアド・ステートメント を使う(パラメータ化されたクエリ)
- オプション #2: ストアード・プロシージャ―を使う
- オプション #3: ユーザーからの入力は全てエスケープする
追加の防御策:
- 追加で強化すべき: 最小限の権限で実行する
- 追加で検討すべき: 入力はホワイトリスト方式で検証する
これらの防御原則に対し WordPress では、$wpdb->prepare()
や esc_sql()
、あるいは 役割りと権限 を検証する current_user_can()
などを準備することで、開発物をセキュアにするプログラミング環境を提供しています。
故に、これらの原則に従って対応する関数を正しく使っていれば、SQLi は起きないハズなんですが…
次の例は「思い込み」に起因すると思われる SQLi の例です。
Custom Contact Forms に見る「思い込み」の盲点
次のコードは、① 設定を SQL ダンプしダウンロードする、② バックアップしておいた SQL をインポートする、という管理者向け機能のコードの一部です。
if (!is_admin()) { ... $custom_contact_front = new CustomContactFormsFront(); } else { ... $custom_contact_admin = new CustomContactFormsAdmin(); ... add_action('init', 'adminInit'); } /* 設定のエクスポート、インポート(管理者向け)*/ function adminInit() { $this->downloadExportFile(); $this->downloadCSVExportFile(); $this->runImport(); }
上記コードには、最低でも5つの問題点が考えられます。
- 「管理者」かどうかの検証を
is_admin()
に頼っている - 予め想定された経路が前提で、別経路でアクセスされた場合を想定していない
- 生の SQL 文を出力する仕様のため、DB のプレフィックスが暴露される
- SQL のエクスポート、インポートに際し、権限を検証、限定していない
- インポートでは、任意の SQL 文が実行できる
結果、誰でも DB の書き換えが可能という深刻な脆弱性が生じてしまいました。
1. について言えば、ここでの is_admin()
の使い方は必ずしも間違いじゃないと思います。ただし 2. においては、「正規にログインした管理者がダッシュボードからアクセスする」という「予め想定された経路」以外にも、admin-ajax.php
や admin-post.php
を直接叩くという経路があり得ます。これらには admin
の名が付いてはいますが、管理者以外でも機能する仕様となっており、おまけに is_admin()
を true
にします。
よって、ある条件で admin-ajax.php
にアクセスすると SQL ダンプが始まってしまうという、「想定外の経路」が存在する構造になっていたのです。
Sucuri が作者に連絡するも応答がなかったため、WordPress のセキュリティ・チームが急遽、対策版をリリースしました。その内容は、単に
へのフックを削除し、init
adminInit()
の実行を阻止するというものでした。
仮に SQLi の防御原則に則った実装をするなら、以下のようにすべきでしょう。
current_user_can('manage_options')
で権限を検証し、- データベースへの入出力はすべてパラメータ化、
- さらに入出力時には、値の検証と無害化をした上で、
- 適切に組み立てられエスケープされた安全な SQL 文を実行する。
この原則が見逃されてしまった要因には、is_admin()
への誤った「思い込み」が盲点となった事が挙げられるのではないでしょうか。フォーラムでのディスカッション が、そのことを如実に物語っているように思います。
また同じ「思い込み」の元凶として、admin_init
フックがあります。テーマ Platform の 任意の PHP コードが注入できてしまう脆弱性 や、プラグイン MailPoet の 任意のファイルがアップロードできてしまう脆弱性 が同種の例として報告されています。
「admin
の名が付いたファイルには気を付けろ!」ってことですネ。
XSS
XSS の詳しい話は 専門家 に譲りますが、ほとんどの場合、外からやって来るデータ(DB から読み出したものを含む)の検証漏れか、出力前のエスケープ漏れでしょう(「安全なウェブサイトの作り方」によると、前者は「保険的対策」、後者は「根本的対策」とされています)。
まずは、「これじゃあ漏れも仕方ない」的な例を紹介します。
Blubrry PowerPress に見る「見過ごし」の例
下の表を見れば分かりますが、Blubrry PowerPress では、最初から XSS が未対策だったワケじゃありません。
PHP | htmlspecialchars() |
esc_html() |
esc_attr() |
||
---|---|---|---|---|---|
ファイル数 | 行数 | ||||
対策前 | 42 | 36280 | 274 | 82 | 153 |
対策後 | 33 | 22807 | 160 | 53 | 131 |
対策前後のコードの一例を以下に示しますが… 何と言うか… 一言で表せば、追跡するのがとても厄介なコードです(すいません、想像してください )。そして至る所にパッチが当てられ、その対策の慌てっぷりがヒシヒシと伝わってきます 。
/* before */ <input type="hidden" ... value="<?php echo empty($_POST['tab']) ? 0 : $_POST['tab']; ?>" /> /* after */ <input type="hidden" ... value="<?php echo empty($_POST['tab']) ? 0 : intval($_POST['tab']) ); ?>" /> /* before */ powerpress_page_message_add_error( ..., $_POST['feed_slug']) ); /* after */ powerpress_page_message_add_error( ..., esc_html($_POST['feed_slug']) ) ); /* before */ echo '<rawvoice:metamark type="'. $MetaMark['type'] .'"'; /* after */ echo '<rawvoice:metamark type="'. esc_attr($MetaMark['type']) .'"'; /* before */ '<em>'. htmlspecialchars($post_title) .'</em>', '<em>'. $feed_slug .'</em>' ); /* after */ '<em>'. htmlspecialchars($post_title) .'</em>', '<em>'. htmlspecialchars($feed_slug) .'</em>' );
こういったヤッツケ仕事的パッチワークの問題点は、「対策漏れ」がないかの検証が難しいこと、そして htmlspecialchars()
と esc_html()
の混在など、統一感のない対策方針が果たして正しいのか判断がつかない事ではないでしょうか?
私には、混在(要は、省略されたパラメータの扱い)が直ちに問題となる例題は示せませんし、実際、大丈夫なのかもしれません。しかし WordPress という系の中では、コアチームが時間をかけて整合してきた関数群を使うべきだと思っています(例えば esc_html()
では 厳密性の高い htmlspecialchars()
の使い方 がされている)。
追記:徳丸さんの 『例えば、PHPを避ける』以降PHPはどれだけ安全になったか | 徳丸浩の日記 に の第3パラメータについて詳しく解説されていました。
入力の検証と出力のエスケープ、およびモデルとビューの分離
「見過ごし」を防ぐ上で大事なことは、「モデル」における「入力の検証」と、「ビュー」における「出力のエスケープ」と言った具合に、見通しのよい設計と実装をする事だと思います(注:ここでは MVC の話をしているつもりはありません)
ちょっと厳しい言い方ですが、先のコードはこれら2つが混沌としているため、「対策漏れ」が起きて当然と言われても仕方ありません。
Codex の Validating Sanitizing and Escaping User Data と Data Validation(日本語版)には、XSS や SQLi、DT など、様々な攻撃を阻止するための入出力に関する検証と無害化の関数群がリストアップされています。
ただし一部、日本で語られている考え方やネーミングと、多少の違いがあるので要注意です。例えば sanitize_text_field()
が、エスケープ処理を施す esc_*()
関数群と同じ「出力のサニタイジング」に分類されていたり、不要なタグやコードを削除する「入力の検証(クリーニング)」の関数群が sanitize_*()
というネーミングになっていたり、といった具合です。
いずれにしても、見通しの良い設計と共に、これらの関数群をコードの文脈に合わせて適切に使うことが肝要でしょう。
最後に蛇足ですが、IPA セキュア・プログラミング講座 にある「スクリプトが動作する箇所」の図と WordPress の esc_*()
関数との対応を取ってみました。
図を書いてみて分かったのですが、expression
攻撃を阻止する関数は無さそうなので、外から入力されたデータを style
属性に注入するのは避けた方が良さそうですネ。
HD FLV Player に見る「見過ごし」の例
FLASH PLAYER PLUGIN の古いバージョンと PHP バージョン 5.3.4 以前との組み合わせには、任意のファイルがダウンロードできるという脆弱性が存在します。
Sucuri の報告 には、以下の様な download.php
のコードが掲載されています。
$finename = $_GET['f']; header('Content-disposition: attachment; filename=' . basename($filename)); readfile($filename);
実際にはこれほど単純ではなく、$finename
に対して、絶対パスの追加と拡張子の検証が行われています。しかし、ディレクトリ・トラバーサルの検証(validate_file()
で可能です)や NULL バイト攻撃 の検証が見過ごされたため、一撃で破られるシングル・フェイルとなってしまいました。
また download.php
が WordPress とは無関係に単独で実行出来るようになっていたという点も問題でしょう。wp-load.php
を読み込んだり、admin-ajax.php
や admin-post.php
経由で nonce や権限を検証するなど、WordPress の仕組みを使うべきだったのではないか、と思います。
権限昇格(PE)
WordPress における nonce を一言で表せば、「今、このページにアクセスしているユーザーだけが知りえる秘密の情報」です。詳しくは拙作の WordPressプラグインのコーディングでありがちな10の間違いと設計時に考慮すべきこと
や 初歩からわかるWordPressのnonceでAjaxをセキュアに実装する方法
を参照してください。
でもその nonce だって漏れるんです。漏れると、単なるユーザーが管理者の権限にまで昇格しちゃうんです。次は、そんな脆弱性の紹介です。
UpdraftPlus に見る「思い込み」の盲点
登録ユーザーのダッシュボード上で wp-admin/?action=hogehoge
にアクセスすると hogehoge_handler()
にフックされるようにしたとします。
add_action( 'admin_action_hogehoge', 'hogehoge_handler' );
このハンドラは wp-content/plugin/hogehoge/admin.php
で定義され、「何か」する目的で、ブラウザに次の様な出力をするものとします。
function hogehoge_handler() { ... <?php ... <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce('hogehoge-secret-nonce');?>"> ... ?> }
この登録ユーザー向け秘密情報のタネである 'hogehoge-secret-nonce'
が、管理者向けのタネと共通で使われていたりするとちょっと心配な事になります。いわゆる「秘密情報の漏洩」ですネ。
さらに別の管理者向けハンドラでは、check_admin_referer('hogehoge-secret-nonce')
で nonce の検証はするものの、管理者権限を持っているかどうかを検証をしなかったりすると、いわゆる「権限昇格」が起きてしまうのです。
UpdraftPlus が正にこのパターンにハマってしまいました。また同様の脆弱性は、あの有名な WPtouch でも 報告されています。
これらも一種の「思い込み」と言えるでしょう。
ファイル・インクルージョン(FI)
通常 FI は、ファイルシステム系の関数に、検証が十分でない入力を与える事で起きる脆弱性です。2014年9月には、file_get_contents()
を介して 任意のファイルがダウンロードできしまう という Slider Revolution の LFI が報告されました。
Slider Revolution に見る「思い込み」の盲点
管理者向けのある機能に対し、admin-ajax.php
を経由した次の様な攻撃で、任意のファイルが漏洩します(本サイトにも未だに時々やってきます)。
http://victim.com/wp-admin/admin-ajax.php?action=revslider_show_image&img=../wp-config.php
Slider Revolution では、管理者用クラスの中で admin-ajax.php
に対するアクション・ハンドラが登録されます。しかし同ハンドラでは、以下の様な検証が全く行われないまま、汎用のユーティリティ・クラスを呼び出していました。
- お約束なハズの nonce を検証していない
- 実行しようとしているユーザーの権限を検証、制限していない
- ディレクトリー・トラバーサルやパスなど、入力の検証と無害化をしていない
問題の構造は冒頭の SQLi と同じですネ。admin-ajax.php
のネーミングに由来する「管理者向け」という「思い込み」が原因です *。想定が「管理者」だとしても、何らかの脆弱性で権限昇格だってあり得るんだから、他の検証を省略できるってことにはならないのです!
* 断定の根拠: 有料販売もしているプラグインですョ、上記の様な基本的な検証が全く実装されていないなんて、あり得ないじゃないですか!
まとめ
今回分析した案件の多くが、admin
の名前が付く関数やアクションフックに対する「思い込み」によるものと推察(一部は断定)しました。また数の多い XSS では、「見過ごし」が起き難い設計が重要との認識を新たにしました。
心構えとしては、(本記事のタイトルのように)常に攻撃側/非攻撃側という2つの側面から、コードの1行1行に「盲点がないか」を問いかけるってことでしょうか。
とどのつまり、性善説じゃダメで、次のような性悪説を元に開発に臨むというのが個人的な結論です。
- 入力は汚染されている
- DB は改ざんされている
- 秘密の情報は漏洩する
- 攻撃は想定外の経路から来る
- 管理者権限は回避、奪取される
- 実際に今、ページにアクセスしているワケではない
これらを全て防ぐのって大変なことですが、こと WordPress という エコシステム に関して言えば、防御に必要な仕組みはコア開発の方々が揃えてくれているので、後は「使いこなし」ってことで、この記事を〆たいと思います。