初歩からわかるWordPressのnonceでAjaxをセキュアに実装する方法

一般的な nonceNnumber used ONCEWeblio 英和辞典 によると、ナンス 又は ノンス と発音)は、Web サイト、特にデータベースに不可逆な変化を引き起こす、意図しない、あるいは重複したリクエストから Web サイトを保護するための、セキュリティが目的のワンタイム・トークンです。

WordPress の nonce は、管理画面に対する CSRF 攻撃英語版 Wikipedia によると、シーサーフ と発音)対策として広く知られているかと思いますが、意外にも Ajax との組み合わせを解説した日本語記事が見当たらなかったので、「いまさら」の共有です。

本記事では、単に Ajax の Tips 的な実装方法だけではなく、CSRF 攻撃の原理や対策についても少しだけ触れたいと思います。またその上で、Ajax に nonce が必要なパターンについて言及します。

さらに、WordPress 3.5 から導入された新しいフィルターフックの使い道を探ってみたいと思います。

CSRF攻撃の具体例

Wikipedia を参考に、次のシナリオを実際に試してみました。

自サイトに管理者としてログインしている時に、悪意ある外部のサイトに誘導され、自サイトに意図しないリクエストをしてしまう

CSRF攻撃の実験

CSRF攻撃の実験

罠ページは Referer の指す WordPress ページへアクセスし、管理画面の URL を推定します(この例では wp-admin/options.php)。また iframe に仕組まれた攻撃用ページが、フォームの hidden フィールドに攻撃用パラメータ(例えば「設定を保存する」)をセットし、onload と共に管理画面にリクエストを POST します。

この仕掛けはブラウザのフォームを使うところがミソで、クッキーやら何やらも一緒に送出することになるので、認証されたままとなります。通常 iframe は見えないよう偽装されるので、実際には攻撃されたことにさえ気付かないでしょう。

この実験では管理画面が丸見えになっていますが、ブラウザに 同一生成元ポリシー に関するバグがない限り、これらの情報が攻撃側に漏れることはありません(クロスドメインなコンテンツを取り込んだ iframe の DOM にはアクセスできない)。

それゆえ、この CSRF 攻撃は失敗します。管理画面の「設定を保存する」ボタンには、秘密情報としてアクセス毎に値が変わる nonce が設定されていて、これを攻撃側が読み取れず、hidden フィールドにセットできないからです。

また余談ですが、WordPress 3.1.3 からは、クリックジャック 対策として X-Frame-Options レスポンスヘッダ が導入されており、クロスドメインな iframe には、ログイン画面および管理画面を表示すら出来なくなっています(上の実験では外しています)。

しかし単に表示されないというだけで POST は実行されるので、安心せずにしっかりと正しい対策方法を学びましょう 🙂 。

WordPressのnonce

次の図 は、「予見不可能な秘密情報」としての nonce を含む、認証の一般的なモデルで(ダイジェスト認証 に用いられているそうです)、特に CSRF 対策だけを対象とした仕組みではありません。

一方 WordPress の nonce は、「予見不可能な秘密情報」という特性はそのままに、CSRF 対策専用としてより単純化されています。cookie ベースの認証の上にアドオンされていると思うと分かり易いでしょう(少なくとも私には)。

要は、(hidden を読み取る以外に)秘密情報を予見できなければ、攻撃は成功しないというワケです。

WordPress では、nonce は厳密な「ワンタイム」ではなく、24時間は再利用が可能となっています(ブラウザの戻るボタンで戻っても有効)。

ただしその値は、攻撃者にとって「予見不可能な秘密情報」であると同時に、それが「意図」された操作であるかどうかがチェックできなければなりません。それ故 nonce を生成する関数 wp_create_nonce() には「意図」に相当する「アクション名」を指定します。さらに内部では時刻とユーザー ID が加えられ、wp-config.php に定義された長〜い NONCE_KEYNONCE_SALT をソルト *1 にしたハッシュ値としています。

もし「意図」が合わないか、リクエスト元の URL($_SERVER['REQUEST_URI'] または $_SERVER['HTTP_REFERER'])が異常な場合、「本当に実行していいですか ? もう一度お試しください。」という念押しのメッセージが表示されます。

このように WordPress では、データベースへの保存など重要な場面で、通常の認証や権限のチェックだけではスルーしてしまう CSRF 攻撃を、nonce で防ぐのがお約束となっているのです。

  • *1: ソルトに関しては、ここ の説明が簡潔で分かり易いです。ただし CSRF の場合、ユーザーが誘導されなければ攻撃が成立しないので、「ソルトは、総当たり的にハッシュ値を計算する ブルートフォース攻撃 のような手段にはほとんど効果はない」は当てはまりません。

Ajaxの場合

第1章では管理画面への攻撃例を示しましたが、リクエスト先を wp-admin/admin-ajax.php に変えれば、Ajax による操作への攻撃が可能となります。

その操作が DB の更新を伴わないのであれば nonce は使わなくてもよいでしょう。事実、ページ送りの Ajax プラグインは nonce を設定していません。逆に閲覧数を数えて DB を更新する WordPress Popular Posts では nonce が設定されています。

以下に、ユーザーの種類と nonce が必要なページのパターンを整理してみました。

  ログインユーザー専用ページ *2 一般公開向けページ *3
DB 更新あり DB 更新なし DB 更新あり DB 更新なし
ログインユーザー 必要 原則不要 *4 必要 原則不要 *5
非ログインユーザー 必要 不要
  • *2: 管理画面のページを含みます
  • *3: ログインユーザーと非ログインユーザーを両立させるには、キャッシュ・プラグインの使い方に注意しなければならないでしょう
  • *4: CSRF 攻撃だけでログインユーザーの秘密情報が漏洩することはありませんが、他の脆弱性との複合を考えれば、使用すべきかと思います
  • *5: 公開ページであっても、ログインユーザー専用の情報を表示する場合には *4 と同様、使用すべきかと思います。

Ajax には Ajax の セキュリティ上の注意すべき事項 が多数あります。特に XSS 脆弱性 があると秘密情報が漏洩し、せっかくの CSRF 対策が台無しになる可能性もあります。使われる文脈と目的に合わせ、抜け目なく実装しましょう 😛 。

nonceを組み合わせたAjaxの実装方法

やっと本題です 😉 。ここからは myajax プラグインの実装を想定し、5つのステップと各々のポイントを解説していきます。

GitHub にも プラグインのサンプル を置きましたので、参考にしてください。

ステップ1:wp_localize_script()でグローバル変数を設定する

まずは Ajax に必要な nonce などの情報を受け渡すためのグローバル変数を設定します。

function myajax_register_scripts() {
	$handle = 'myajax-script';
	wp_enqueue_script( $handle, plugin_dir_url( __FILE__ ) . 'js/script.js', array( 'jquery' ) );

	$action = 'myajax-action';
	wp_localize_script( $handle, 'MyAjax', array(
		'url' => admin_url( 'admin-ajax.php' ),
		'action' => $action,
		'nonce' => wp_create_nonce( $action ),
	) );
}
add_action( 'wp_enqueue_scripts', 'myajax_register_scripts' );

ここでのポイントは、Ajax のリクエスト先を wp-admin/admin-ajax.php に設定すること、および次節にも関係する action に「意図」に相当するアクション名を設定することです(WordPress が内部で $_REQUEST['action'] を参照します。他の要素名は任意です)。

wp_enqueue_script()wp_localize_script() により、<head> 内に以下の様な <script> のブロックが生成されます。

<script type='text/javascript'>
/* <![CDATA[ */
var MyAjax = {
	"url": "http:\/\/example.com\/wp-admin\/admin-ajax.php",
	"action": "myajax-action",
	"nonce": "生成されたナンス値"
};
/* ]]> */
</script>
<script type='text/javascript' src='http://example.com/wp-includes/js/jquery/jquery.js'></script>
<script type='text/javascript' src='http://example.com/wp-content/plugins/myajax/js/script.js'></script>

ステップ2:リクエストを送信するJavaScript

jQuery 1.5 以上で使える Promise インターフェース をった、js/script.js の例を示します。

(function ($) {
	function myajax_request() {
		$.post( MyAjax.url, {
			action: MyAjax.action,
			nonce: MyAjax.nonce
		}).done( function ( data, textStatus, jqXHR ) {
			// ここには成功時の処理を記述します
		}).fail( function( jqXHR, textStatus, errorThrown ) {
			// ここには失敗時の処理を記述します
		});
	}
}(jQuery));

ステップ3:admin-ajax.phpからのハンドラを設定する

WordPress ではユーザーの種類と権限を細かく規定することができます。このステップでは、「ログインしている登録ユーザー用」と「閲覧権限しか持たない(no privilege)非ログインユーザー用」の Ajax 用ハンドラを設定します。

$action = 'myajax-action';
add_action( 'wp_ajax_' . $action, 'myajax_submit' );		// ログインユーザー用 Ajax ハンドラの設定
add_action( 'wp_ajax_nopriv_' . $action, 'myajax_submit' );	// 非ログインユーザー用 Ajax ハンドラの設定

注意すべきは、「非ログインユーザー用ハンドラは、ログイン中の登録ユーザーに反応しない」ということです。また管理画面専用の Ajax なら 'wp_ajax_' だけを設定します。

ステップ4:nonceとユーザー権限をチェックする

いよいよレスポンスを返す関数 myajax_submit() の実装です。ここでの重要ポイントは、nonce のチェックとユーザー権限のチェックです。

nonce をチェックする関数は次の3つがあります。

続いて ユーザーの種類と権限 のチェックです。

WordPress では、ユーザーを「特権管理者」から「閲覧者」まで 6種類のグループ に分け、役割りに応じた権限を割り当てています。例えば、自身の投稿やコメントを編集する権限の edit_posts、管理画面を設定する権限 manage_options などといった具合です。

現在閲覧中のユーザーがこの権限を持つかどうかは、current_user_can() で調べます。

以上を合わせ、myajax_submit() は、例えば次のようにします。

function myajax_submit() {
	if ( isset( $_REQUEST['nonce'] ) && wp_verify_nonce( $_REQUEST['nonce'], 'myajax-action' ) ) {
		if ( current_user_can( 'edit_posts' ) ) {
			// 権限を持ったユーザー用の処理
		} else {
			// 権限のないユーザー用の処理
		}
		header( 'Content-Type: application/json; charset=UTF-8' );
		echo json_encode( 結果の配列など );
	} else {
		status_header( '403' );
		echo 'Forbidden';
	}
	die();
}

ステップ5:その他の注意すべきこと

レスポンスの戻り値に注意

check_admin_referer()check_ajax_referer()die('-1') で終了する場合があります。また、4.3章で説明した admin-ajax.php 用ハンドラが登録されていない場合(例えばプラグインが有効になっていないなど)、'0' が返ってきます。

JavaScript 側で適切に処理できるようにしておきましょう。

is_admin()は権限チェックにならない

なぜなら wp-admin/admin-ajax.php へのアクセスは、必ず true になるからです。管理者権限を調べるには、current_user_can( 'manage_options' ) を使いましょう。

キャッシュ・プラグインに要注意

ログインユーザーにはキャッシュ・ページを提供しないよう設定しましょう。

nonce関連のフィルターフック

有効期限の変更

nonce_life フィルターフック で有効期限を設定できます。

function myajax_change_nonce_hourly( $nonce_life ) {
	// nonce 有効期限を1時間に設定する
	return 3600;
}
add_filter( 'nonce_life', 'myajax_change_nonce_hourly' );

非ログインユーザー個別の設定

非ログインユーザー(ID が 0)は、特定ユーザーだけに有効な nonce は設定できません。例えばクッキーやセッションでユーザーを区別し、専用の nonce を生成したい場合は、WordPress 3.5 から導入された nonce_user_logged_out フィルターフック を使います。

function myajax_logged_out_user( $uid, $action ) {
	if ( ! $uid && 'myajax-action' === $action )
		$uid = session_id();
	return $uid;
}
add_filter( 'nonce_user_logged_out', 'myajax_logged_out_user', 10, 2 );

このフィルターフックは、wp_create_nonce()wp_verify_nonce() に作用します。

参考文献

本記事をまとめるにあたり、勉強させてもらった文献です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません