nonceはWordPressのセキュリティ機能のひとつで、ワンタイムパスワードみたいなものです。
そのnonceまわりの処理の関数で、ドキュメントと違う実装をしても正常に動くことがあるので、ソースコードから調べました。
よく分かってなかった関数の使い分けがはっきりしたのが一番の収穫です。
nonceとは何か?
nonceは使い捨てのランダムな値でワンタイムパスワードで使います。
(nonceは英語で『1回きりの値』という意味。)
これを使ってサーバが正当なhttpsリクエストを受信したことを確認します。
(セキュリティのCSRF対策になります。)
nonce値には有効期間があり、タイムアウトしたらその値では認証エラーになるしくみです。
WordPressのデフォルト有効期間は1日です。
nonceのタイムアウト値の変更
デフォルトの1日が長いときは変更しましょう。
add_filter( 'nonce_life', function () {
// 1時間
return HOUR_IN_SECONDS;
});
指定するのは秒です。これだけの定数が使えます。
定数 | 値 |
---|---|
MINUTE_IN_SECONDS | 分 |
HOUR_IN_SECONDS | 時間 |
DAY_IN_SECONDS | 日 |
WEEK_IN_SECONDS | 週 |
MONTH_IN_SECONDS | 月 |
YEAR_IN_SECONDS | 年 |
// 30分
$time = 30 * MINUTE_IN_SECONDS;
// 2時間
$time = 2 * HOUR_IN_SECONDS;
// 3日
$time = 3 * DAY_IN_SECONDS;
// 10週間
$time = 10 * WEEK_IN_SECONDS;
// 半年
$time = 6 * MONTH_IN_SECONDS;
// 5年
$time = 5 * YEAR_IN_SECONDS;
WordPressのnonce
WordPressでは3つの値を管理します。
action | キーを作成するときのnonce名。 認証するときに使う。 |
token (key) | actionからnonceトークンを作成する。 クライアントに渡すnonce値。 (HTMLに貼り付ける。) httpsリクエストが正しいことを確認するため に使うクライアントとサーバーの共通キー。 |
name (query_arg) | httpsのurlの後ろにつけるnonceのクエリ名。 https://xxxxx.com/xxxxxx?name=key |
nonceのしくみ
nonceの流れです。
サーバー (PHP) | クライアント (HTML, JavaScript) | |
---|---|---|
ユニークなactionからトークンを作成。 (actionを覚えておく。) | ||
キーとクエリ名を渡す。 (HTMLに貼り付ける) | ➡ | トークンとクエリ名を受け取る。 |
一旦ここで処理は終了。 | ||
トークンとクエリ名を受け取る。 | ⬅ | 別のhttpsリクエストのurlにクエリ名と トークンを乗せてサーバーに送る。 ( https://xxxxxx.com/xxxx?name=key ) |
クエリ名からトークンを取得。 | ||
覚えていたactionから新しいトークン を作成。 | ||
新しいトークンと受け取ったトークン 比較して検証。 | ||
httpリクエストでnonceを受け取って いない。(NG) 有効期限が過ぎている。(NG) キーがまちがっている。(NG) 検証OK。 | ||
レスポンスをクライアントに返す。 (認証NGならエラー画面など) | ➡ | リクエスト結果を表示する。 |
処理としてはシンプルなんですが、WordPressにはnonceまわりの関数がいくつもあり、また、いくつかの方法があります。
作業 | function |
---|---|
トークン作成 | wp_create_nonce() wp_nonce_url() wp_nonce_field() |
認証処理 | wp_verify_nonce() check_admin_referer() check_ajax_referer() |
リファラ(リクエスト送信元url)作成 | wp_referer_field() |
メッセージ出力 ( 辿ったリンクは期限が切れています。 ) | wp_nonce_ays() |
非推奨 | wp_explain_nonce() -> wp_nonce_ays()を使う。 |
これ、使い分けできるでしょうか?
ボクにはできませんでした。ただ、ドキュメント(Codex)に書いてあるとおりに実装してきただけなので。
ひとつひとつ見ていきましょう。
トークン作成
トークン作成のfunctionは3つ用意されていますが、じっさいはwp_create_nonce()だけです。
ほかの2つは内部でwp_create_nonce()を使っています。
wp_create_nonce() - トークン作成の基本
wp_create_nonce( $action );
action | アクション文字列。 省略可。 デフォルト: -1 |
戻り値 | 作成したトークン |
actionなしでもキーを作成しますが指定が推奨されています。また、Ajax通信では別のところでもactionを使います。
ユニーク文字列を指定します。
$action = 'test-token';
echo wp_create_nonce( $action );
ba9006e56b
ほかとはちがってトークン文字列だけを作るので、AjaxなどJavaScriptなので重宝します。
actionを省略するとどうなる?
actionを省略するとaction=-1でトークンが作られ使いまわします。
ユーザーのログイン機能があるときは『ユーザーログイン・セッションごと』に同じトークンを使います。
(セッション単位はサイトへのログイン中(ログアウトするまで)の間。)
個人的には、公開サイトのトークンはactionなしでもいいと思います。クレジットカード情報やパスワードなど、秘密情報がからんでないかぎり。
さっきも言いましたがAjax通信では必須です。WordPressの管理画面はAjax通信でフォーム(ボタン操作)の情報を送るので、管理画面はactionが必要です。
wp_nonce_url() - URLクエリのトークン
wp_nonce_url( $actionurl, $action, $name );
actionurl | トークンを追加するurl |
action | アクション文字列。 省略可。 デフォルト: -1 |
name | urlのトークン・クエリ名。 省略可。 デフォルト: _wpnonce |
戻り値 | トークンを追加したurl |
wp_create_nonce()は、自分でurlやhtmlを作らないといけない面倒くささがあります。WordPressでは、トークンが入ったURLやHTMLまで作ってくれるfunctionが用意されています。
wp_nonce_url()は、urlにトークンのクエリを追加します。
$url = 'https://exmaple.com';
sprintf('<a href="%s">example.com</a>', wp_nonce_url( $url ));
(urlは内部リンクの相対パスも使える。)
<a href="https://exmaple.com>?_wpnonce=c242418284">example.com</a>
処理の内部でesc_html()を使っているので、HTMLのエスケープ処理は不要です。
wp_nonce_field() - フォームのトークン
wp_nonce_field( $action, $name, $referer, $echo );
action | アクション文字列。 省略可。 デフォルト: -1 |
name | urlのトークン・クエリ名。 省略可。 デフォルト: _wpnonce |
referer | フォームの送信元urlのhiddenを付けるか? 省略可。 デフォルト: true |
echo | 出力するか?変数に入れるか? 省略可。 デフォルト: true, 出力する |
戻り値 | トークンを追加したurl |
フォームのトークン用のfuncsionです。デフォルトではrefererが付いているので、同時に送信元urlのhidden(リファラー)も追加します。
フォームの操作ではリファラーチェックが必須なのでありがたいですね?
(urlからの直リクエストとかの不正なフォーム操作を防ぐ。)
色んなパターンで実行してみましょう。
referer | true |
echo | true |
<form method="post">
<?php wp_nonce_field(); ?>
</form>
<form method="post">
<input type="hidden" id="_wpnonce" name="_wpnonce" value="c242418284">
<input type="hidden" name="_wp_http_referer" value="/example/">
</form>
リファラーは相対パスです。外から実行することはないので当たり前ですけど。
(外から実行されると危ない。)
referer | true |
echo | false |
<form method="post">
<?php
$field = wp_nonce_field( -1, '_wpnonce', true, false);
echo $field;
?>
</form>
<form method="post">
<input type="hidden" id="_wpnonce" name="_wpnonce" value="c242418284">
<input type="hidden" name="_wp_http_referer" value="/example/">
</form>
この条件では、リファラーを出力しないと日本語ドキュメントに書いていますが情報が古いようです。日本語ドキュメントしか読まない人は注意しましょう。
referer | false |
echo | true |
<form method="post">
<?php wp_nonce_field( -1, '_wpnonce', false ); ?>
</form>
<input type="hidden" id="_wpnonce" name="_wpnonce" value="c242418284">
referer | false |
echo | false |
<form method="post">
<?php
$field = wp_nonce_field( -1, '_wpnonce', false, false );
echo $field;
?>
</form>
<input type="hidden" id="_wpnonce" name="_wpnonce" value="c242418284">
自分でリファラーを追加することもできます。
<form method="post">
<?php wp_referer_field(); ?>
</form>
<input type="hidden" name="_wp_http_referer" value="/example/">
<form method="post">
<?php
$field = wp_referer_field( false );
echo $field;
?>
</form>
<input type="hidden" name="_wp_http_referer" value="/example/">
認証処理
トークンを作成して、ブラウザからトークンを送信するところまでできました。
認証処理のfunctionも3つ用意されていますが、じっさいはwp_verify_nonce()だけが認証処理です。
のこりの2つは内部でwp_verify_nonce()を実行しているので。
wp_verify_nonce() - 認証処理の基本
wp_verify_nonce( $nonce, $action );
nonce | トークン。 必須。 |
action | アクション文字列。 省略可。 デフォルト: -1 |
戻り値 | 1: トークンの経過が0-12時間 2: トークンの経過が12-24時間 false: 認証エラー |
ブラウザから渡されたトークンとサーバーが覚えているactionを使って認証します。
サンプルを実行する前に準備をします。サンプルの投稿ページ(slug: nonce-test)を作成します。
ホームから投稿ページにリンクを貼るときトークンをつけて認証します。
<?php
$url = '/nonce-test';
$action = 'action-test';
printf('<a href="%s">link auth test</a>', wp_nonce_url( $url, $action ));
?>
<?php
$action = 'action-test';
$nonce = $_REQUEST[ '_wpnonce' ];
$auth = wp_verify_nonce( $nonce, $action );
?>
<p>認証結果: <?php echo $auth; ?></p>
<?php
if ( $auth ) {
// 正常なページを表示
} else {
// 認証エラー画面を表示
}
?>
認証エラー処理の変更
認証処理のエラー発生時の後処理を追加することもできます。
// 本来は直書きしない。別ファイルやクラスオブジェクトにする。
add_action( 'wp_verify_nonce_failed', function( $nonce, $action, $user, $token ) {
wp_nonce_ays( $action );
die();
}, 10, 4);
check_admin_referer()の後処理をコピーして作りました。
// 先頭に入れないと正常ページが見えてしまう。
<?php
$action = 'action-test';
$nonce = $_REQUEST[ '_wpnonce' ];
$auth = wp_verify_nonce( $nonce, $action );
?>
// 正常なページの処理。エラー用はいらない。
(index.phpは変更なし。)
エラー表示はこんな感じ。
(wp_verify_nonceの$nonce値を変えてわざとエラーを起こす。)
メッセージは英語ですがソースコードは__()で囲っているので、翻訳ファイルに追加すれば日本語化できます。
WordPress5.3.2の日本語版で動作確認しました。ここまでマニアックなところまでは翻訳されていないのでしょう。
The link you followed has expired. | フォローしているリンクの有効期限 が切れています。 |
Please try again. | もう一度やり直してください。 |
『Please try again』はリンクになっていて元のページに戻ります。
WordPress.orgリファレンス
WordPress Codex 日本語
check_ajax_referer() - Ajaxの認証
ajax用のfunctionが用意されています。
WordPressのForm・POST送信はajaxで実装します。個人的にはnoticeまわりで一番使ってる気がします。
check_ajax_referer( $action, $query_arg, $die );
action | アクション文字列。 省略可。 デフォルト: -1 |
query_arg | https通信クエリのトークン名。 ($_REQUESTの変数名) デフォルト: false |
die | 認証エラーのとき強制終了するか? (403を返す。) デフォルト: true |
戻り値 | 1: トークンの経過が0-12時間 2: トークンの経過が12-24時間 false: 認証エラー |
wp_verify_nonce()とのちがい
check_ajax_referer()の処理は、ほぼwp_verify_nonce()です。
ちがうところは条件と後処理の2つだけ。
actionを指定しないと警告が出る
ajaxではactionをほかのところでも使うので、未指定にする人はいないと思います。気にしなくていいです。
(Ajax受信処理のアクション・フックに使う。)
httpsクエリ名が決まっている
ajaxのトークンはhttps(POST)のクエリから取得します。
$_REQUESTの変数名
$_REQUEST['*****']
httpsリクエストから任意のクエリ名(パラメータより)を探し、なかったら ' _ajax_nonce' を探します。それでもなかったら強制的に ' _wpnonce' にしてwp_verify_nonce()でチェックします。
(もちろん任意のクエリ名はあらかじめ決めておく。)
許可しているクエリ名
- 任意のクエリ名
- _ajax_nonce
- _wpnonce
httpsクエリにnonceが付いていないと即エラーになります。
認証処理を追加する
デフォルトだと、トークンのクエリ名が決まっているだけで、認証処理はwp_verify_nonce()とまったく同じです。
後処理のアクションフック(check_ajax_referer)が用意されているので追加しましょう。
オススメはURLリファラー・チェックです。
(URLリファラー・チェックは特定のurlからしかajax通信を許可しない。)
あとでサンプルコードに実装します。
認証処理の終わらせ方も実装できます。
ランダムのnonce値の作成では、URLリファラー(送信元URL)は使っていません。
url固有のnoticeにするには、作成時のactionにurlをつける工夫が必要です。
もっと確実にしたければ、URLリファラー認証を追加します。
関数名にだまされてはいけない!
function名は "check_ajax_referer" ですが、URLリファラー・チェックはしていません。
(後処理で追加できるが。)
check_admin_referer()に影響されました。中身はnonceリファラーチェックだけです。
サンプルコード
サンプルコードを実行しましょう。テストなのでJavaScriptはテンプレートファイルに直書きしました。
本来はjsファイルを作ってそこに書いてください。
<form id="ajax-test">
<?php
$action = 'ajax_test';
wp_nonce_field( $action );
?>
<input type="hidden" name="action" value="<?php echo $action; ?>" />
<button>Ajax Test</button>
</form>
<script type="text/javascript">
(function($) {
$(function() {
$('#ajax-test').submit(function(event){
"use strict";
// HTMLで送信しない。リロードするため
event.preventDefault();
$.ajax({
type: "POST",
url: '<?php echo admin_url('admin-ajax.php'); ?>',
dataType: 'text',
data: $(this).serialize()
}).done(function(data, textStatus, jqXHR) {
console.log(data);
}).fail(function() {
console.log('ajax-test error!!');
});
});
})
})(jQuery);
</script>
HTMLの<form>のsubmit動作は使いません。ボタンを押すと必ずページのリロードが起きるからで、ページ遷移が起きているのと同じだからです。
(ボタンを押すだけでhttpsリクエストが発生する。)
ajaxは非同期処理なのでページ遷移は起きません。HTMLはこうなります。
<form id="ajax-test">
<input type="hidden" id="_wpnonce" name="_wpnonce" value="6b66f8f2cf" />
<input type="hidden" name="_wp_http_referer" value="/nonce-test/?_wpnonce=e8c24f76b9" />
<input type="hidden" name="action" value="ajax_test" />
<button>Ajax Test</button>
</form>
<script type="text/javascript">
(function($) {
$(function() {
$('#ajax-test').submit(function(event){
"use strict";
// HTMLで送信しない。リロードするため
event.preventDefault();
$.ajax({
type: "POST",
url: 'https://my-themes.test/wp-admin/admin-ajax.php',
dataType: 'text',
data: $(this).serialize()
}).done(function(data, textStatus, jqXHR) {
console.log(data);
}).fail(function() {
console.log('ajax-test error!!');
});
});
})
})(jQuery);
</script>
action, _wpnonceはフォームの送信データで必須です。
次に、ajaxの受信処理を入れましょう。add_action()を使うので処理はテーマやプラグインの最初の方に書きます。
(テーマのfunctions.phpやプラグインファイル。)
// 本来は直書きしない。別ファイルやクラスオブジェクトにする。
/**
* ajaxのリクエスト受信後処理
*/
function my_ajax_test() {
$action = 'ajax_test';
$query = '_wpnonce';
$result = check_ajax_referer( $action, $query, false );
echo 'ajax OK code[' . $result . ']';
die();
}
add_action( 'wp_ajax_ajax_test', 'my_ajax_test' );
add_action( 'wp_ajax_nopriv_ajax_test', 'my_ajax_test' );
/**
* ajax認証の追加
*/
add_action('check_ajax_referer', function( $action, $result ) {
// URLリファラーチェックの追加
if ( $result ) {
$referer = strtolower( wp_get_referer() );
if ( 'ajax_test' === $action ) {
// 特定の投稿ページからだけ許可する。
$slug = 'nonce-test';
$post = get_posts('name=' . $slug);
$url = get_permalink( $post[0]->ID );
if ( ! strpos( $referer, $url ) ) {
$return = false;
}
}
}
// 終わり方の変更
if( ! $result ) {
wp_nonce_ays( $action );
die();
}
}, 10, 2 );
URLリファラーチェックも追加しました。CSRF対策の強化です。
check_admin_referer() - 管理画面の認証
管理画面用にfunctionが用意されています。
ただ中身はwp_verify_nonce()と同じで、認証処理が追加できる程度のちがいなので、管理画面用とは言えません。
check_admin_referer( $action, $query_arg );
action | アクション文字列。 省略可。 デフォルト: -1 |
query_arg | https通信クエリのトークン名。 ($_REQUESTの変数名) 省略可。 デフォルト: _wpnonce |
戻り値 | 1: トークンの経過が0-12時間 2: トークンの経過が12-24時間 |
認証エラーのときリターンしない。
(wp_nonce_ays()でメッセージ出力して処理終了)
もともとnonceの関数じゃなかった
もともとこの関数はnonceチェックではなく、urlリファラー・チェック関数でした。
(だから関数名がcheck_admin_referer)
管理画面のurlからでないと、httpsリクエストを受けつけない仕様です。
それがWPバージョン2.5からnonceチェックに変わります。いまは、下位互換のために以前の仕様が残っています。
(httpsリクエストにnonceがついていないとき、urlリファラー・チェックになる。)
check_ajax_referer()の関数名の原因はこいつ
check_ajax_referer()は最初からnonceを使ったチェックです。ただ、check_admin_referer()がnonceに対応する前に作られたため、関数名が影響されました。
WP バージョン | |
---|---|
1.2.0 | check_admin_referer() 作成。 処理はURLリファラー・チェック。 |
2.0.3 | check_ajax_referer() 作成。 最初からnonceでリファラー・チェック。 nonceにurlは使っていない。 |
2.5.0 | check_admin_referer() 変更。 nonceでリファラー・チェック。 nonceにurlは使っていない。 |
"check_admin_nonce()" や "check_ajax_nonce()" にしてほしいところです。
Codexドキュメントの一部訂正
Codex日本語版の戻り値の説明ではこんなことが書いてあります。
好ましい使い方の場合、nonce が有効であれば true を返します。非推奨の使い方の場合、現在のリクエストが 管理画面 から参照されていれば true を返します。
Codex日本語版より
非推奨はパラメータなしの方法で、urlリファラー・チェックを行う方法です。
ただしこれには訂正が必要です。httpsリクエストのクエリに " _wpnonce =*****" でnonce値が入っていると、urlリファラーチェックではなく、nonceチェックを実行します。
nonceを送らずに、check_admin_referer()を使うと、urlリファラー・チェックになる。
ことに注意しましょう。
公開ページでも使える
nonceをhttpsリクエストにつけて送るとき、check_admin_referer()の中身はwp_verify_nonce()と同じです。
(ちがいは認証エラー時の処理の終わり方だけ。)
テーマのindex.phpなどテンプレートでも使えますが意味がありません。wp_verify_nonce()と同じなんだもん。
認証処理が追加できる。
check_ajax_referer()と同じように、add_action()で認証処理が追加できます。
(あとでサンプルで実装します。)
処理の終わらせ方も変更できます。
サンプルコード
最初の認証処理の基本のコードを変更します。
<?php
$url = '/nonce-test';
$action = 'action-test';
printf('<a href="%s">link auth test</a>', wp_nonce_url( $url, $action ));
?>
ここに変更はありません。
// 先頭に入れないと正常ページが見えてしまう。
<?php
$action = 'action-test';
$auth = check_admin_referer($action, '_wpnonce');
?>
// 正常なページの処理。エラー用はいらない。
認証処理をcheck_admin_referer()に変更します。
check_admin_referer()の認証処理に、管理画面以外は拒否するように処理を追加しましょう。
(公開サイトでも使えるのは意味がないので。)
// 本来は直書きしない。別ファイルやクラスオブジェクトにする。
add_action( 'wp_verify_nonce_failed', function( $nonce, $action, $user, $token ) {
wp_nonce_ays( $action );
die();
}, 10, 4);
/**
* admin認証の追加
*/
add_action('check_admin_referer', function( $action, $result ) {
// urlリファラーチェックの追加
if ( $result ) {
$adminurl = strtolower( admin_url() );
$referer = strtolower( wp_get_referer() );
// 管理画面のurlからだけ許可する。
if ( false === strpos( $referer, $adminurl ) ) {
wp_nonce_ays( $action );
die();
}
}
}, 10, 2 );
urlリファラーの処理はcheck_admin_referer()からコピーしました。
これを実行すると追加認証でエラーになるはずです。一方、管理画面のどこかの設定画面で使えば認証は通過します。
(管理画面での実行サンプルは割愛。)
下位互換のurlリファラー・チェック処理の前に差し込むので、追加処理でdie()を使うと、urlリファラー・チェックは行いません。
そもそもnonceを送信すると処理しないので、どうでもいいですが。
気になること
いまのWordPressのnonceは、タイムアウト値は設定できますが、1回使ったらnonceを無効にすることができません。
(wp_delete_nonce()とかいうfunctionがない。)
タイムアウト値を限りなく短くすれば近いことはできそうですが、有効期間のあいだに連続してリクエストを送ることができます。
あとAjaxの認証では、トークンを作成するactionもクライアントで見えます。これを秘密鍵にできないかなとも思います。
(actionを秘密鍵、nonceを公開鍵にしてほしい。)
(現状actionは、Ajax受信処理のフックにも使うので、クライアントから送らざるを得ない。)