WordPress プラグイン開発の脆弱性対策

この記事は、Jon Cave 氏が意図的に作成した「意図的な脆弱プラグイン」をもとに、それがどの様な危険性をはらんでおり、どのように修正すべきかを解説するものです。もし、まだこの「意図的な脆弱プラグイン」のコードを見ていないのでしたら、この記事を読む前に目を通しておくと良いでしょう。

なおこの記事は、Jon Cave 氏の記事「How to fix the intentionally vulnerable plugin」を分かり易くするために、少しだけ説明を追加したものです。

「意図的な脆弱プラグイン」の有効化

実際にプラグインを有効化して、どのような攻撃を行うとどうなるのかを一つ一つ検証して、該当するコードを修正して行こうと思います。

このプラグインは、有効化するだけでは意図的に動作しないようになっています。動きを確認するためには、plugin.phpに define(‘LOAD_INTENTIONAL_VULNS’, 1); の1行を付け加えます。

define('LOAD_INTENTIONAL_VULNS', 1);

// Safety precautions are out of the way so load the actual stuff
if (defined('LOAD_INTENTIONAL_VULNS') && LOAD_INTENTIONAL_VULNS) {
	include( dirname(__FILE__) . '/vulnerable.php' );
}

また、再有効化の時にエラーが出るので、plugin.php の34行目を以下の様にして、テーブルの作成は一度しか行わないように修正します。

		if( !get_option( 'dvp_unknown_logins' ) ){
			require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
			dbDelta( $sql );
		}
		update_option( 'dvp_unknown_logins', 1 );

このプラグインを有効化すると、データベースに login_audit というテーブルが一つ作成されます。そして、管理パネルの「ツール」のサブメニューに、「Failed logins」が追加されていることを確認できれば有効化が成功です。

このプラグインは、意図的に脆弱性を持っている危険なプラグインです。公開しているサイトでは有効化しないようにしましょう。また、テストサイトであっても、有効化して検証を行った後は、必ず停止するのを忘れないようにしましょう。

SQLインジェクション

これは、攻撃者によってデータベースクエリが変更されてしまう可能性のある脆弱性です。これを悪用することにより、攻撃者は、データベースの情報を読み取り、変更、削除などのコマンドをデータベース·サーバ上で実行する事が可能になります。基本的に、ユーザ入力をそのままクエリに含めると、それが悪意を持ってフォーマットされた入力だった場合、クエリの構文を変更されてしまう可能性があるのです。

最初のSQLインジェクションの脆弱性は、42行目の$wpdb->prepare()関数の誤った使用方法によって発生します。

$wpdb->query( $wpdb->prepare( "INSERT INTO login_audit (login, pass, ip, time) VALUES ('$login', '$pass', '$ip', '$time')") );

prepare()関数を使用する際は、第1引数のクエリー文の中で %s や %d といったプレースホルダーを使用すべきです。ユーザー入力をそのままクエリー文として決定してしまっては、その入力を解析することができません。変数(ユーザー入力)は、第2引数以降の別々の引数として渡され、クエリーに含まれる前に適切にエスケープされることになります。WordPress 3.5 移行はこの関数の第2引数が必須となり、上記の様な使い方ではエラーとなるようになりました。

正しい使い方はこのようになります。

$wpdb->query( $wpdb->prepare( "INSERT INTO login_audit (login, pass, ip, time) VALUES (%s, %s, %s, %s)", $login, $pass, $ip, $time ) );

これは、$wpdb->prepare() の使い方の間違いを示しているものです。実際には、フックから渡されたパスワードはサニタイジングされており、クエリー内でクオートにより括られているので、攻撃をしてクエリーを壊すことはできませんでした。しかしながら、$wpdb->prepare() は非常に重要な関数ですので、常に正しい使い方心がけるようにしましょう。WP3.9 でのデバッグモードでは、「Notice: wpdb::prepare が誤って呼び出されました。wpdb::prepare() のクエリ引数にはプレースホルダーが必要です。 詳細は WordPress のデバッグをご覧ください。」というNoticeが出るよになったようです。

他に、2つのSQLインジェクションの脆弱性が、102行目と127行目にあります。それらは、クオートで囲まれていないユーザー入力の、間違ったエスケープによって引き起こされます。esc_sql()関数は、入力を安全にエスケープするだけであって、クオートで囲ってはくれません。ユーザー入力が数値であると保障されない限り、以下の様な決め打ちは危険です。

$log = $wpdb->get_row( "SELECT * FROM login_audit WHERE ID = " . esc_sql( $id ), ARRAY_A );
// ... and ...
$wpdb->query( "DELETE FROM login_audit WHERE ID = " . esc_sql( $_POST['id'] ) );

%d プレースホルダを使ったprepare()関数を使用して修正するとこの様になります。

$log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM login_audit WHERE ID = %d", $id ), ARRAY_A );
// ... and ...
$wpdb->query( $wpdb->prepare( "DELETE FROM login_audit WHERE ID = %d", $_POST['id'] ) );

他に、int型にキャストするかabsint()関数を使用して数値入力をサニタイズすることも可能です。

クロスサイト・スクリプティング

クロスサイト・スクリプティング(XSS)は、Webアプリケーションで最も一般的なセキュリティ上の脆弱性の1つです。XSSもまた、WordPress プラグインによく見られる脆弱性なので、「意図的な脆弱プラグイン」にもたくさん入れてみました。XSSは、よく「永続型」と「反射型」に分類されます。安全でないユーザー入力がデータベースに保存され、その後他のユーザーのリクエストに対して出力されるのであれば、XSS の脆弱性は持続型と言えます。一方、安全でないユーザー入力に対してその場で危険なレスポンスを返してくるものは、反射型として知られています。反射型XSSの脆弱性を悪用するためには、攻撃者は、悪意のあるスクリプトのトリガとなるリンクを被害者にクリックさせるよう、巧みに細工を施さなくてはいけません。しかし、永続型XSSの脆弱性は、攻撃者に危険なスクリプトの入力を許し、無防備なユーザーがこのサイトを訪れ、問題ないように見えるリンクをクリックすることを待ち構えます。

「意図的な脆弱プラグイン」には、2つの持続的XSSの脆弱性があります。それはdvp_view_all_logs()とdvp_view_log()の中に存在します。それらの関数は、データベースからデータをエスケープすること無く直接出力します。このデータは全て、dvp_log_failed_login()でのユーザーが原因です。 例えば、攻撃者がパスワードのフィールドに <script>alert(1)</script>と入力することで、意図的にログインを失敗させ、データベースにJavaScriptを保存させることができます。
これに対処するには、データベースからのデータを出力する場合、esc_html()やesc_attr()など適切なエスケープ関数を使用する様、dvp_view_all_logs()とdvp_view_log()を修正すべきです。 またデータベースに保存する前に、KESEを使用することでデータを無害化することができますし、HTMLタグを気にしないのであればstrip_tags()を使用して無害化することもできるでしょう。

反射型XSSの例も複数あります。104行目と115行目では、$_GET[‘id’]が元となる変数$idがエスケープ無しで出力されています。104行目はesc_html()で修正すべきです。そして115行目はesc_attr()で修正すると良いでしょう。その他の問題は、現在のURLを取得するために116行目で使用されている$_SERVER[‘PHP_SELF’]です。この環境変数は、攻撃者が値を制御することができ、属性を壊しスクリプトを実行する危険なHTMLを含ませることができるという、非常に一般的な問題を抱えています。正しいURLを取得するには、menu_page_url()の様なWordPressの関数を使用すべきです。

クロスサイト・リクエスト・フォージェリー

その他、Webアプリケーションで一般的な脆弱性にはクロスサイト・リクエスト・フォージェリー(CSRF)が挙げられます。この種の攻撃は、意図しない認証チェックハンドラの要求を悪用するものです。悪意のあるWebサイトは、ユーザーが認証されている別のサイトに対してリクエストを送信することができます。脆弱なターゲットサイトは、有効な認証Cookieを送信されるため、この要求を受け入れてしまいます。しかしユーザーは、そのアクションの実行に気が付かない場合が多いのです。
WordPressのプラグインは、nonceを使用することによって、この種の攻撃を防御することができます。nonceは、攻撃者によって予測することはできませんが、ワードプレスで確認することができる、アクションごとのランダム文字列です。そのため、要求が有効なnonceを含んでいないのであれば、それを拒否することができるのです。

「意図的な脆弱プラグイン」は、CSRFを防御する為にnonceを利用しているかのように見えますが、CSRFを可能にしてしまう2つのミスを犯しています。まず、アクションパラメータを渡さずにnonceの生成と検証を行うのは安全ではありません。114行目と123行目は、第1引数にアクションを指定する編集が必要です。WP_DEBUGを有効にすると、第1引数にアクション名を指定せずにcheck_admin_referer()を使用した場合、このミスがキャッチされnoticeが通知されることに注意してください。
2つ目のミスはもっと繊細です。137行目では下記の様にチェックが行われています。

if (isset($_REQUEST['nonce']) && ! wp_verify_nonce($_REQUEST['nonce'], 'dvp_settings'))
    // ... failed nonce check

このチェックの問題点は、nonceリクエストパラメータの存在が前提となっていることです。これは、攻撃者がこのパラメータを省略して、簡単にnonceの検証をパスできてしまう事を意味しています。

if ( ! isset($_REQUEST['nonce']) || ! wp_verify_nonce($_REQUEST['nonce'], 'dvp_settings')
    // ... failed nonce check

管理パネルであれば、wp_verify_nonce()の代わりにcheck_admin_referer()を使用することを強く推奨します。

権限チェックの欠如

ログ削除ハンドラでの権限チェックの欠如は、権限昇格攻撃を可能にします。権限のチェックが行われない為、ログインしたユーザーは皆 login_audit テーブルからデータ行を削除する事ができてしまいます。 ログ削除ハンドラは、_nopriv アクションにフックされている訳ではないので、商品されていないユーザーは権限昇格攻撃を利用することはできません。しかし、これもまた深刻なセキュリティ上の障害です。ですので、権限が要求されるハンドラの実行時や管理ページ上での権限チェックに、適切な current_user_can() の使用をいつも忘れてはいけません。

リダイレクト後の終了障害

サーバーサイドでは、リダイレクトの処理が実行された後も、実際にはそのスクリプトが終了するまで処理は続いています。これは、リダイレクトの後には常に exit か die() が必要であることを示しています。140行目でこれを行わないと、権限チェックやCSRF 防御は(たとえロジックエラーが無かったとしても)役に立ちません。偽造リクエストや権限の低いユーザーの要求は、たとえ条件分岐によりリダイレクトさせても、意図的に処理を終了させないと、その後の設定更新の処理は実行されてしまうのです。(check_admin_referer() は、チェックが通らないとdie() を実行するので、こう言った偽造リクエストの問題は無くなります。)リダイレクトが使用されているほかの場所も、スクリプトの終わりでリダイレクトが実行されているので脆弱ではありませんが、そこにも同様に、終了処理を加える習慣を身に付けて下さい。

オープン・リダイレクト

リダイレクト関連としては、130行目の削除ハンドラ内にオープン・リダイレクトの脆弱性が存在します。このプラグインの意図することは、ログイン失敗のリストにユーザーをリダイレクトするには、旧来の wp_redirect() の代わりに wp_safe_redirect() を使うべきだという事です。ただ、このシチュエーションでは、リクエスト・パラメータを使ったリダイレクトはやめて、menu_page_url() や admin_url() を使ったURLへリダイレクトするのも良いでしょう。

IP フォージェリー

環境変数 X-Forwarded-For はユーザーが変更可能なので、dvp_get_ip() 関数は、不正なIpアドレスを見抜く事ができません。これはいろんな災いをもたらします。セキュリティーを目的としたログのはずが、逆に、正規アドレスから来たふりをする攻撃者を追跡審査する機能を低下させてしまいます。このアクセスが確保されれば、バイパス可能なのは明白です。このプラグインのケースでは、SQLインジェクションや永続的XSS につながる可能性があります。

おわりに

プラグイン制作者がミスを犯してしまいそうな事はたくさんあります! まず第一に、アクセスしてくるユーザーは信用できないという事を覚えておかなくてはいけません。ユーザーがコントロール出来得るものは、常に検証しエスケープしてください。

私は、皆さんにとって、この記事がプラグイン・セキュリティーの理解への手助けになる事、そしてエクササイズとして役立つことを祈っています。

※原文「How to fix the intentionally vulnerable plugin