WordPressのセキュリティ(というかWebサイトのセキュリティ)対策って本当に難しいですね。
特にWordPressでカスタムコードを使ったり、自身でテーマやプラグインを作る際には、具体例が少ない(というかケースが多岐にわたっているので具体的に提示するのが難しい)ので、解説ページなどを見つけても理解に苦しむことも多いです。
フロントエンド・バックエンドに限らず、WordPress本体+デフォルトテーマを使用するだけで、他のテーマを利用したり、プラグインを追加したり、カスタムコードを追加することが一切なければ、将棋の初期配置と同じく「一番堅牢な状態」に保たれるよう、開発者の方が尽力してくれています(逆を言えばありがたくこれを利用させていただいているだけです)。
将棋に例えましたが、将棋も初期配置が一番堅牢な状態で、一手指すごとに隙が生まれ、相手に攻め込まれやすくなります。逆を言えば駒が限られている中で相手の王将を取るゲームなのですから、攻撃するためには仕方ないことでしょう。
これと同じように、WordPressもテーマを変えたり、プラグインをインストールしたり、カスタムコードを追加することで、最初の布陣に比べれば、相手(ハッカー)などに攻め込まれる機会を増やすことになります。
よく公式のプラグインを使えば..なんてのを耳にしますが、公式で配布されているものも、初期リリース時に厳格にチェックされているだけで、以降のアップデートすべてで同様のことが行われているとは限りません(その証拠に、後から脆弱性が指摘されたりすることが多いです)。
ただこれも、プラグインやテーマの作者がより安全にサイト運営ができるように努力してくれているものを、便利に利用させてもらっているだけだとということを絶対に忘れてはならないと思います。
こんな感覚でWordPressを使っていれば、「〇〇プラグインはダメだ」とSNSなどで揶揄したり、サポートで暴言を吐いたりといったことはなくなるのでしょうね。
..と前置きはこの位にしておきましょう(笑)。
さて、今回は、データの入出力時のセキュリティ対策の1つ、「nonce」についてのお話です。
「nonce」という言葉は、多分テーマのカスタマイズや、テーマ・プラグインの作成をした(しようとした)ことがある方ならご存じでしょう。
でも、WordPress公式の開発リソースにある「Nonces」を読んでもチンプンカンプンじゃないですか?
恥ずかしながら、私もその一人です。なので、本ページでは、私の「nonce」に対する知識の深耕と、具体例を含めた使い方の備忘録を兼ね、公開しています。
セキュリティに関することですから、本格的に学びたい!完全理解したい!という方はプロの方の指南を受けたり、スクールに通ったりすることをおすすめします。
「nonce」が行っていること
nonceが行っていることは以下の通り(と私個人は認識しています)。
- 入力フォームなどにnonceと呼ばれる隠し入力項目を設ける
- その入力フォームで更新などのアクションを行う際に、以下のチェックをし、クリアできなければ無視する
- そのフォームなどにnonceフィールド(隠し入力項目)があるか
- nonceフィールドに設定された一時的な値(時限的に設定された値)と、アクション時の値が一致しているか
平たく言えば、その場所(今回の場合は入力用の窓)以外から入力したものはすべて無視するという振る舞いをさせることができます。
要するに、データベースの保存時にデータから悪いものを省いたり、HTML出力時に悪影響を及ぼす可能性のあるものを無害化する「エスケープ」や「サニタイズ」とは異なる方法でセキュリティアップを図る手法ということです。
従ってnonceを使用するケースとしては、管理画面・閲覧画面問わず、何かを入力する場所(文字入力・チェック入力など)すべてが対象と考えるといいでしょう。
なお、フロントエンド(表示画面側)から、何かの送信をさせるという場合には、本ページの簡易的な検証では不味いので、フロントエンドからの送信を行う(問い合わせフォーム系など)ためのプラグインを素直に使った方がよいと思います
nonceの利用ケース(投稿や固定ページごとにJSやCSSを使う)
以下のコードは、文末で参考ページに挙げさせていただいた「WordPressの投稿や記事ページごとに個別のCSSとJSを追加する方法【カスタマイズ】」に掲載されているコードを元に、各所コメントの追記やコードの追加・変更を行ったものです。
コードを、有効化しているテーマのfunctions.phpへ追加すれば、以下のような機能を追加することができます。
- 投稿や固定ページの編集画面下部にメタボックス(入力窓)を表示する
- nonce機能を使ってデータの保存時に検証を行い、不正なものは排除する
/***** 投稿・固定ページごとのJS出力 *****/
/*** 投稿・固定ページにメタボックス(入力欄)を出力 ***/
if ( !function_exists( 'sample_custom_js_hooks' ) ){
function sample_custom_js_hooks() {
add_meta_box( 'sample_box_custom_js', 'カスタム JS', 'sample_custom_js_input', 'post', 'normal', 'high' );
add_meta_box( 'sample_box_custom_js', 'カスタム JS', 'sample_custom_js_input', 'page', 'normal', 'high' );
}
}
add_action( 'admin_menu', 'sample_custom_js_hooks' );
/*** メタボックスの内容(コールバック) ***/
if ( !function_exists( 'sample_custom_js_input' ) ){
function sample_custom_js_input() {
global $post;
echo '<input type="hidden" name="sample_custom_js_noncename" id="sample_custom_js_noncename" value="'.wp_create_nonce('sample-custom-js').'" />';
echo '<textarea name="sample_id_custom_js" id="sample_id_custom_js" rows="20" cols="30" style="width:100%;">'.get_post_meta($post->ID,'_sample_custom_js',true).'</textarea>';
}
}
/*** データを保存 ***/
if ( !function_exists( 'sample_save_custom_js' ) ){
function sample_save_custom_js($post_id) {
//nonceチェックに失敗したら実行しない
if (!wp_verify_nonce($_POST['sample_custom_js_noncename'], 'sample-custom-js'))
return $post_id;
//一括更新では実行しない
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
return $post_id;
//更新処理
$custom_js = $_POST['sample_id_custom_js'];
if(!empty($_POST['sample_id_custom_js']))
update_post_meta($post_id, '_sample_custom_js', $custom_js);
else delete_post_meta($post_id, '_sample_custom_js', $custom_js);
}
}
add_action( 'save_post', 'sample_save_custom_js' );
/*** HTMLソース上へ出力 ***/
if ( !function_exists( 'sample_insert_custom_js' ) ){
function sample_insert_custom_js() {
//出力する内容を変数としてセット
$js_meta_data = get_post_meta(get_the_ID(), '_sample_custom_js', true);
//個別ページでのみ、カスタムフィールドの内容を出力
if(is_single() || is_page()){
echo $js_meta_data;
}
}
}
add_action( 'wp_footer','sample_insert_custom_js' );
※コードを分かりやすくするため、同じキー(文字列)を使う場所は、同じ色で装飾しています
まんまで、自身のサイトに機能追加ができてしまいますね。でも..何も知らずにコードを流用するのはいかがでしょう、怖くないですか?(怖くない方は鵜呑みで構いませんが責任も持ちません)
テーマのfunctions.phpへの追記に慣れていない方、トラブル発生時に対処のできない方は、安易に上記コードを導入しないでください(トラブル等が起こっても一切責任を負いません)
では、以下でコードの解説をしていきます。
コードとnonceの機能についての解説
ここからは前項で紹介したサンプルコードの説明をしていきます。
サンプルでは、コード中で同じ文字列を使う部分などは色分けしてありますので、複数のカスタムフィールドを扱う場合などは一意になるように、また、関数を分ける場合は関数名を一意にしましょう
サンプルコードでは、大別して、3つの役割がありますので、それぞれを分解して解説していきます。
完全理解しているわけではないので認識の誤っている部分があるかも知れませんがご容赦ください
編集画面へメタボックス(入力窓)を追加する
以下の部分が投稿や固定ページへメタボックスを追加する部分です。
/*** 投稿・固定ページにメタボックス(入力欄)を出力 ***/
if ( !function_exists( 'sample_custom_js_hooks' ) ){
function sample_custom_js_hooks() {
add_meta_box( 'sample_box_custom_js', 'カスタム JS', 'sample_custom_js_input', 'post', 'normal', 'high' );
add_meta_box( 'sample_box_custom_js', 'カスタム JS', 'sample_custom_js_input', 'page', 'normal', 'high' );
}
}
add_action( 'admin_menu', 'sample_custom_js_hooks' );
/*** メタボックスの内容(コールバック) ***/
if ( !function_exists( 'sample_custom_js_input' ) ){
function sample_custom_js_input() {
global $post;
echo '<input type="hidden" name="sample_custom_js_noncename" id="sample_custom_js_noncename" value="'.wp_create_nonce('sample-custom-js').'" />';
echo '<textarea name="sample_id_custom_js" id="sample_id_custom_js" rows="20" cols="30" style="width:100%;">'.get_post_meta($post->ID,'_sample_custom_js',true).'</textarea>';
}
}
投稿編集画面へメタボックス(カスタムフィールドを入力する窓)を追加する基本的なコードです。「_sample_custom_js」というフィールド名を持つデータを保存するためのフォーム(入力窓)を「sample_custom_js_input」という関数から呼び出しています。
残念ながらこのコードの塊の意味が不明という方は、この先読み進めてもさらに分からなくなると思いますので、メタボックスの追加方法に関するいろいろな文献を参照することをおすすめします
コード中に以下の記述があります。
echo '<input type="hidden" name="sample_custom_js_noncename" id="sample_custom_js_noncename" value="'.wp_create_nonce('sample-custom-js').'" />';
echo '<textarea name="sample_id_custom_js" id="sample_id_custom_js" rows="20" cols="30" style="width:100%;">'.get_post_meta($post->ID,'_sample_custom_js',true).'</textarea>';
実際にコード追加して、投稿編集画面を開いてみると、窓は1つしかないのに、2つの入力項目を呼び出しています。
よく見るとそれぞれinputタグとtextareaタグの2つがあり、input側の入力タイプはhidden(非表示)になっていますね。
このinput項目は、非表示の入力項目を目に見える入力欄の上に置き、コード中の「wp_create_nonce」という組み込み関数を使用して、「sample-custom-js」という暗号(合言葉)を設定しています。
つまり、保存されるフィールド名は「_sample_custom_js」ですが、「sample-custom-js」という合言葉を使って照合しなさいという風に変更しているのです。
入力された値を更新してデータベースへ書き込む
以下の部分が入力窓に入力した値を保存する部分です。
/*** データを保存 ***/
if ( !function_exists( 'sample_save_custom_js' ) ){
function sample_save_custom_js($post_id) {
//nonceチェックに失敗したら実行しない
if (!wp_verify_nonce($_POST['sample_custom_js_noncename'], 'sample-custom-js'))
return $post_id;
//一括更新では実行しない
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
return $post_id;
//更新処理
$custom_js = $_POST['sample_id_custom_js'];
if(!empty($_POST['sample_id_custom_js']))
update_post_meta($post_id, '_sample_custom_js', $custom_js);
else delete_post_meta($post_id, '_sample_custom_js', $custom_js);
}
}
add_action( 'save_post', 'sample_save_custom_js' );
冒頭の「//nonceチェックに失敗したら実行しない」の部分で、フォームに入力されたフィールド名に合言葉が付いているか、つまりは投稿編集画面の入力窓から入力され、保存されたものかをチェックしています。別の場所からのものであれば無視され、保存されないようになっています。
次の「//自動保存では実行しない」の行は、自動保存されたときにカスタムフィールドの保存が走り、データが空になったりしてしまう不具合を防ぐお約束のコードです。
最後の「//更新処理」のところで、項目の入力が空だったらフィールドごと削除してデータベース内にゴミが残らないようにしつつ、入力があればデータを更新するようにしています。
この処理によって、不正に値をデータベースに保存されるのを防ぐことができているんですね。
HTMLソース上へデータを呼び出す
最後に、HTMLソース上へ出力する部分です。
/*** HTMLソース上へ出力 ***/
if ( !function_exists( 'sample_insert_custom_js' ) ){
function sample_insert_custom_js() {
//出力する内容を変数としてセット
$js_meta_data = get_post_meta(get_the_ID(), '_sample_custom_js', true);
//個別ページでのみ、カスタムフィールドの内容を出力
if(is_single() || is_page()){
echo $js_meta_data;
}
}
}
add_action( 'wp_footer','sample_insert_custom_js' );
入力されたデータは保存した際に安全が確認できているので、あとは出力するだけです。
【注意】フィールド名について
ここまでのサンプルコードでは、保存されるカスタムフィールド名(メタキー)を「_sample_custom_js」と先頭に「_」をつけて投稿編集画面のカスタムフィールド一覧上で非表示にしています。
これは、nonceチェックによって、カスタムフィールドの一覧上で値を変更しても、正規の場所(メタボックス)からの入力でないことから反映されない=表示しても意味がないからです。
ただ、今まで使っていたメタキーに対して単純に「_」を付加してしまうと、当然ながらメタキーそのものが変わってしまい、今までのカスタムフィールドに入力した値が使えなくなったり、消失してしまったかのように見えることがあります(実際にはデータベース内に旧メタキーで保存されていて、消失することはまずありません)。
前述したように、編集できないのだから隠しておいた方がいいとは思いますので、「_」を付けた形で保存・呼び出しができるようにした上で、今までのメタキーを「_」付に変更してしまうのが一番妥当な策だと思います。
ただ、手動で1つ1つ変更していくのは大変なので、以下のプラグインを使って、今までのメタキーそのものを変更するのが手っ取り早いでしょう。
長く更新されていないプラグインですが、PHP8.0、WordPress6.2RC4でも動作しましたので、メタキーの変更のためだけに有効化するなら問題ないと思います。
使用方法については、カスタムフィールド名(post meta key)の変更ができるプラグイン「Simple Post Meta Manager」で説明してますので、読んでみてから判断してください。
nonceが機能しているかを試してみよう
おさらいになりますが、nonce検証は「その場所から入力・更新されたもの以外はすべて無視する」という機能でしたね。
前項で紹介したコードをコピペしてnonce検証ができるようになった!と安心する前に、本当にきちんとnonce検証が機能しているかを試してみましょう。
前項で紹介したコピペ用のコードと、以下のコードを入れ替えてください。
/***** 投稿・固定ページごとのJS出力 *****/
/*** 投稿・固定ページにメタボックス(入力欄)を出力 ***/
if ( !function_exists( 'sample_custom_js_hooks' ) ){
function sample_custom_js_hooks() {
add_meta_box( 'sample_box_custom_js', 'カスタム JS', 'sample_custom_js_input', 'post', 'normal', 'high' );
add_meta_box( 'sample_box_custom_js', 'カスタム JS', 'sample_custom_js_input', 'page', 'normal', 'high' );
}
}
add_action( 'admin_menu', 'sample_custom_js_hooks' );
/*** メタボックスの内容(コールバック) ***/
if ( !function_exists( 'sample_custom_js_input' ) ){
function sample_custom_js_input() {
global $post;
echo '<input type="hidden" name="sample_custom_js_noncename" id="sample_custom_js_noncename" value="'.wp_create_nonce('sample-custom-js').'" />';
echo '<textarea name="sample_id_custom_js" id="sample_id_custom_js" rows="10" cols="30" style="width:100%;">'.get_post_meta($post->ID,'sample_custom_js',true).'</textarea>';
}
}
/*** データを保存 ***/
if ( !function_exists( 'sample_save_custom_js' ) ){
function sample_save_custom_js($post_id) {
//nonceチェックに失敗したら実行しない
if (!wp_verify_nonce($_POST['sample_custom_js_noncename'], 'sample-custom-js'))
return $post_id;
//一括更新では実行しない
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
return $post_id;
//更新処理
$custom_js = $_POST['sample_id_custom_js'];
if(!empty($_POST['sample_id_custom_js']))
update_post_meta($post_id, 'sample_custom_js', $custom_js);
else delete_post_meta($post_id, 'sample_custom_js', $custom_js);
}
}
add_action( 'save_post', 'sample_save_custom_js' );
/*** HTMLソース上へ出力 ***/
if ( !function_exists( 'sample_insert_custom_js' ) ){
function sample_insert_custom_js() {
//出力する内容を変数としてセット
$js_meta_data = get_post_meta(get_the_ID(), 'sample_custom_js', true);
//個別ページでのみ、カスタムフィールドの内容を出力
if(is_single() || is_page()){
echo $js_meta_data;
}
}
}
add_action( 'wp_footer','sample_insert_custom_js' );
※コードの違いは、カスタムフィールド名(メタキー)に「_」を付加しているかどうかです。これが意味するところは..前項をご覧ください
サンプルコードをコピーして保存したら、投稿(固定ページ)の新規作成を行ってください。
編集画面の本文下に以下のような入力窓が表示されるようになります(そうなっていなかったらコードの設置のどこかで間違いがありますので、前項に戻って確認ください)。
まずはここに何か(「1234」など何でもいい)を入力して、下書き保存し、画面をリロード(再読み込み)してください。
先ほどの窓の中に入力した文字列(「1234」など)が表示されていますね。これで、メタボックス(入力窓)の表示、データの更新が問題なくできていることが確認できます。
続いて、投稿(固定ページ)編集画面に、以下の手順で、通常は非表示になっているカスタムフィールド欄を表示させます。
- 編集画面右上の三点リーダー(「・・・」が縦並び)をクリックし、「設定」をクリックします
- 「パネル」をクリックし、「カスタムフィールド」を有効にします(再読み込みを促されるのではいを選択します)
これで編集画面の下に、入力窓と共にその投稿(固定ページ)に設定されているカスタムフィールドの名前(メタキー)と値が一覧表示されるはずです。
そこには、下図のように名前が「sample_custom_js」で値が「1234」という行がありますね。
もう一度説明しますが、nonce検証は「その場所から入力・更新されたもの以外はすべて無視する」という機能でしたね。
ということは、この欄で文字列を変更して保存しても、反映されないはず..。
試しにカスタムフィールド欄の値(上図では「1234」)を別の文字列に変更して、下書き保存後、画面をリロードしてみてください。
...いかがでしょう?元の「1234」のままになっていますよね?
これがnonce検証のパワーです。
ちなみに、そのままの状態で、入力窓(下図の窓)で値を変更して保存してみてください。
...どうでしょう?きちんとデータが更新されますね。
これがnonce検証のパワーです。セキュリティアップを実感できましたよね?
ちなみにこの項の冒頭で、検証のためにコードを入れ替えてもらいました。そこではカスタムフィールド名(メタキー)から「_」を省いてますよとも説明しましたね。
もう少し戻ってもらうと、「_」はカスタムフィールドの一覧上で非表示にするためのものと説明しました。
この検証で気づいたと思いますが、カスタムフィールド一覧上で値を変更しても反映されない=一覧に表示しても意味がないということで、最初から「_」を付けて一覧に表示しないようにしておいた方が、後から更新できないなぁ..なんて戸惑うことがないということなんですね。
サンプルコードで動作検証してみよう
前項で、投稿・固定ページへのメタボックス追加と更新、nonceを用いたデータの保存、そして、データのHTML出力ができるようになったはずです。
ただ、このままでは本当に動作するかどうか不安ですね。
そこで以下のコードを使って、実際に動かしてみましょう!
うまく動作していれば、ある程度スクロールすると表示されるコンテンツが実装できます
メタボックスに簡単なjQueryを追加する
投稿の新規作成画面を開いて、以下のコードを本文末尾にある「カスタムJS」メタボックスへ追加します。
<!-- カスタムJS -->
<script type="text/javascript" src="/wp-includes/js/jquery/jquery.js" id="jquery-core-js"></script>
<script>
(function(jQuery) {
jQuery(function () {
var display = function () {
var fixContact = jQuery('.is_fix_test');
scrollHeight = jQuery(document).height();
scrollPosition = jQuery(window).height() + jQuery(window).scrollTop();
scrollPositionTop = jQuery(window).scrollTop();
//画面上から200ピクセルを超えたときだけ表示させる
if((scrollPositionTop > 200)){
fixContact.fadeIn(400);
} else {
fixContact.fadeOut(400);
}
};
jQuery(window).on("scroll", display);
});
})(jQuery);
</script>
これはページ冒頭から200px分スクロールすると、「is_fix_test」というクラスの付いたコンテンツを表示させ、それ以下のスクロール量になると非表示にしなさいというコードです
カスタムHTMLへコンテンツを追加する
投稿編集画面へ「カスタムHTML」ブロックを追加し、以下のコードを入力します。
<div class="ha-container is_fix_test">
<h2>スクロール後表示のテスト</h2>
<ul>
<li>この部分は一定量スクロール時に表示されます</li>
</ul>
</div>
<style>
.ha-container{
display:none;
}
.ha-container.is_fix_test{
background:#000;
color:#fff;
padding:20px;
}
</style>
プレビューで確認する
下書き保存して、「新しいタブでプレビュー」すると、200pxスクロールさせた段階で以下のようなコンテンツが表示され、スクロール量が200px以下になると非表示になるはずです。
もしも表示されない場合には、以下を再確認ください。
- ずっと表示されている感じがする
▶ヘッダー部分が大きいなど、スクロール量と表示/非表示の切り替えが再現できない場合があります。jQueryコードの中の「scrollPositionTop > 200」の「200」の数字を変更してみてください - スクロール量を変化させても変わりない
▶投稿の下書き保存をすると、入力したコードがデータベースに反映されますので、今一度下書き保存をクリックしてから試してみてください。
参考にさせていただいたページ
本ページの作成にあたり、以下のページを参考にさせていただきました。
最後に
今回サンプルとした、投稿や固定ページへ個別にCSSやJSを追加する機能については、いろいろなサイト・ページで紹介されており、比較的簡単に導入できるので、専用のプラグインの代わりに使われる方も多いのではないでしょうか?
しかしながら、本ページで紹介したように、セキュリティ部分までを補完したコードを紹介しているページは、改めて検索してみても、私が知る限り少ないのではないかと思います。
そう考えると、どこかのページで紹介されているコードは鵜呑みにして、導入できたからOKという判断は、時として、セキュリティリスクを増大させることが多分にあるのかも知れません。
本ページを読んで、今回の機能に限らず、そのあたりも同時に考えていただけると嬉しいです。
こうして分解して、体系を見て、時にはこのように一般公開できるようにしてみると、より理解が深まるのではないでしょうか?
コメントを残す