ノラWEB屋 野良人(のらんど)- 個人営業のWEB屋さん

【WordPress】「本当に実行していいですか?」/「辿ったリンクは期限が切れています。」と怒られないポイント

2018年5月22日

WordPressの管理画面でカスタムフィールド等の保存処理を実装した際に「本当に実行していいですか?」または「辿ったリンクは期限が切れています。」と怒られないようにするための書き方です。

check_admin_refererによるnonceの検証周りの実装を確認していきます。

    どんな時に訊かれるか

    WordPressに「本当に実行していいですか?」と訊かれるタイミングとしてはざっと見た感じ以下のものがあるようです。

    テーマインストール時やファイルのアップロード時などにファイル容量の制限に引っかかる

    本稿では扱いませんが、php.iniでアップロードの上限を変更したら解決するようです。

    nonce検証時のcheck_admin_refererのエラー

    本稿の主題です。3行でいうと以下の通り。

    • WordPressにはCSRF対策としてnonceという仕組みが用意されている
    • wp_nonce_fieldなどで生成したnonceをチェックする関数のうち、管理画面からのnonceの検証にはcheck_admin_refererを用いる
    • check_admin_refererでは、nonceが有効ではない場合に適切なメッセージ(デフォルトは「本当に実行していいですか?」/「辿ったリンクは期限が切れています。」)を表示して終了する(最近のアップデートで「本当に実行していいですか?」→「辿ったリンクは期限が切れています。」メッセージが変更になった模様https://core.trac.wordpress.org/changeset/42811/#file22

    そんなわけで、開発中に「本当に実行していいですか?」/「辿ったリンクは期限が切れています。」というエラーがでる場合はほとんどnonce周辺の問題で発生しているのではないかと思います。

    「本当に実行していいですか?」/「辿ったリンクは期限が切れています。」というエラー文はどこから来るか

    ここです。

    wp_nonce_ays

    check_admin_refererの内部では、検証エラー時にwp_nonce_aysを呼び出す処理が行われています。この関数では、デフォルトで「本当に実行していいですか?」(WordPress4.9.5以降は「辿ったリンクは期限が切れています。」)のメッセージを表示します。

    管理画面やAjax以外のコンテキストからのnonceを検証するwp_verify_nonceでは、結果がfalseの場合にwp_nonce_aysを呼び出すというお作法(?)もあるそう

    怒られる実装のポイント

    save_postアクションで必要ない処理の時にもcheck_admin_refererのnonce検証が走っている

    PHP
    add_action( 'save_post', 'save_mydata' );
    function save_mydata(){
    	if ( ! empty( $_POST ) && check_admin_referer( 'name_of_my_action', 'name_of_nonce_field' ) ) {
    		update_option( 'mydata', $_POST('mydata') );
    	}
    }
    

    上記はCodexのcheck_admin_refererのページの用例をsave_postでフックして、自作カスタムフィールドか何かの保存処理を書いたものです。

    検証前のチェックが必要

    Codexの用例はプラグインなどで追加した自作オプションページなどを想定した例です。

    投稿などに追加したカスタムフィールドの保存を行う場合は、nonceの検証の前に何らかのチェックを行う必要があるので、この用例をそのままフックしてはいけません。

    save_postの挙動

    save_postアクションでは、あらゆる投稿の保存時に毎回このチェックが走ります。そのため関係ない投稿や固定ページの保存時にnonceが確認できないエラーで「本当に実行していいですか?」/「辿ったリンクは期限が切れています。」のメッセージが表示されることになります(あと新規追加時にも同じ処理が走るみたい)。

    値が間違っている

    PHP
    wp_nonce_field( 'name_of_my_action','name_of_nonce_field' );
    ~省略~
    check_admin_referer( 'mistake_action', 'name_of_nonce_field' )
    

    正しい手順で検証を行っていても、アクションやnonceの名前を間違えている場合、当然ながら認証に失敗します。また、一意な名付けが推奨されるので、なるべく他と被らないネーミングにしておきましょう。

    怒られない実装のポイント

    nonceの有無を確認する

    検証する前にそもそもnonceがPOSTされているかどうかを確認します。

    PHP
    add_action( 'save_post', 'save_mydata' );
    function save_mydata(){
    	if( isset( $_POST( 'name_of_nonce_field' ) ) && $_POST( 'name_of_nonce_field' ) ){
    		if( check_admin_referer( 'name_of_my_action', 'name_of_nonce_field' ) ){
    			update_option( 'mydata', $_POST('mydata') );
    		}
    	}
    }
    

    以下のように端折ってしまうと怒られるので注意してください。僕です。

    PHP
    add_action( 'save_post', 'save_mydata' );
    function save_mydata(){
    	if( isset( $_POST( 'name_of_nonce_field' ) ) && $_POST( 'name_of_nonce_field' ) && check_admin_referer( 'name_of_my_action', 'name_of_nonce_field' ) ){
    		update_option( 'mydata', $_POST('mydata') );
    	}
    }
    

    投稿タイプを判別する

    特定の投稿タイプのみに存在するフィールドの場合に有効です。特定の投稿タイプの保存のみにフックすることで、関係ない時に処理をせずに済みます。if( get_post_type() == 'my_post_type' )としてもいいですが、save_post_{post_type}アクションで、特定の投稿タイプの保存時のみにフックすることもできます。

    PHP
    add_action( 'save_post_my_post_type', 'save_mydata' );
    function save_mydata(){
    	if( isset( $_POST( 'name_of_nonce_field' ) ) && $_POST( 'name_of_nonce_field' ) ){
    		if( check_admin_referer( 'name_of_my_action', 'name_of_nonce_field' ) ){
    			update_option( 'mydata', $_POST('mydata') );
    		}
    	}
    }
    

    おすすめ

    上記のようにif文をネストするんじゃなくて、add_meta_boxの用例のように、エラーが出たら何もしない系の書き方もあります。こっちのサンプルの方が、自動保存時の処理とか、ユーザー権限の確認もあってより堅牢なんじゃないかと思います。

    PHP
    add_action( 'save_post', 'save_mydata' );
    function save_mydata(){
    	if( ! isset( $_POST['name_of_nonce_field'] ) ){
    		return;
    	}
    	if( !check_admin_referer( 'name_of_my_action', 'name_of_nonce_field' ) ){
    		return;
    	}
    	if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ){
    		return;
    	}
    	if( isset( $_POST['post_type'] ) && 'page' == $_POST['post_type'] ){
    		if ( ! current_user_can( 'edit_page', $post_id ) ) {
    			return;
    		}
    		if ( ! current_user_can( 'edit_post', $post_id ) ) ) {
    			return;
    		}
    	}
    	update_option( 'mydata', $_POST('mydata') );
    }
    

    まとめ

    なんかの時に、普段使っている保存処理のコードをもっとシュッとした感じに出来ないかと思っていじっていたら「本当に実行していいですか?」と怒られてハマったので調べつつまとめました。

    記事を書きかけの状態でしばらく放置している間にWordPress本体のアップデートでメッセージが変わっていて混乱しました…。記事中の表記がとってつけたような感じになっているのはそのためです。

    nonce周りの処理はちょっとめんどいですが、セキュリティ上重要なところなので、間違いのないようにせねばならぬですね。Codexをきちんと読み込むのが一番良いなと思いました。add_meta_boxのページが勉強になるかなと思いつつ、Codexが全部翻訳されてないので、勉強を兼ねてチマチマ翻訳しています。この作業は大変勉強になるのでおすすめです。日本語版Codexへの参加はこちらから

    このエントリーをはてなブックマークに追加