Wonderful! WordPress

Content-Security-Policy と X-Frame-Options と

ちょっとセキュリティなそなえ

個人でやってる普通のブログやら画像サイトにはあまり必要のなさそうなことだとは思うのだけれど。
自分のサイトも然りで、たぶんこういうことが必要になってくるのは、アクセスユーザーに多くを入力させたりクリックさせたりするサイトなんだろうとは思う。

ただ、ここのところよく使う Chrome DevTools Lighthouse を見ていたら、CSP という文字が目に止まってしまった。
「A strong Content Security Policy (CSP) significantly reduces the risk of cross-site scripting (XSS) attacks.」ー>「強力なコンテンツセキュリティポリシー(CSP)は、クロスサイトスクリプティング(XSS)攻撃のリスクを大幅に軽減します。」ということである。
セキュリティ的に壁を厚くできるということなら、自サイトにあまり必要なさそうなことでも、興味が惹きつけられてしまう。実際のところ、他のサイトでは必要になってくることであろうから、とにかくいろいろやってみておくか!ということにした。

そして、一方の X-Frame-Options
CSP のことを調べていたら、こちらもよく目についたこと。
X-Frame-Options は、「MDN Web Docs X-Frame-Options」によれば、
「HTTP のレスポンスヘッダーであり、ブラウザがページを <frame> <iframe> <embed> <object> の中に表示することを許可するかどうかを示すためのもので、サイトはコンテンツが他のサイトに埋め込まれないよう保証することで、クリックジャッキング攻撃を防ぐために使用することができる。」
と、いうことである。

クリックジャッキングってのは、<iframe> などで埋め込み表示させたページに透明にした要素を重ねるなどして、悪事の潜む要素をだましてクリックさせるという手口とのこと。
まぁ、これも対応してあればそれにこしたことはない、ということであるし、こちらは簡単にできることであるからさっそく適応してみた。

X-Frame-Options はクリックジャッキング攻撃へのそなえ

X-Frame-Options は、レスポンスヘッダーに送信させるだけのことなので、なんてことはなく簡単にできる。
WordPress の場合は、phpheader() を使って、functions.php に書けば WordPress のサイト全体に有効になる。

<?php
	header( 'X-Frame-Options: SAMEORIGIN' );
?>
PHP
CopyExpand

指定している “SAMEORIGIN” はディレクティブと呼ぶそうで、他にもいくつかある。詳しくは先のサイトをご覧あれ。とりあえず、これだけのこと。

そして Content Security Policy ( CSP )

CSP の主たる目的は、 クロスサイトスクリプティング(XSS)攻撃 へのそなえ、というか、完全には防ぐことはできないけれど、軽減させることはできる、ということ。簡単に言うと、サーバーから指定されていないスクリプトはブラウザに実行させないというものだと。

これもレスポンスヘッダーに指定するだけのことなので、指定するだけなら簡単なことなのだけれど、なかなかそのあとが大変、手間がかかることをしなくてはならないのである。サイトによっては、WordPress においては事実上不可能と書いてあるところもあるほど。
まぁ、テーマを自作、プラグインもほとんど使っていないということで、なんとか出来そうだと思った。ただ、ログインページと管理画面においては無理だと思うし、通常は必要も無いと思う。

Content Security Policy のポリシーディレクティブ等の詳細に関しては、以下のサイトなどをどうぞ。他にも詳しく書いてくれているページなどある。
「Content Security Policy Reference」
「コンテンツセキュリティポリシー (CSP) MDN Web Docs」

各サイトの情報と Lighthouse にて表示される推奨内容を参考にして次のように設定した。

<?php
    define( 'CSPNONCE', md5( uniqid( mt_rand(), TRUE ) ) );

    if ( ! is_admin() and 'wp-login.php' !== $GLOBALS['pagenow'] ) {
        header("Content-Security-Policy: script-src 'nonce-" . CSPNONCE . "' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: https:; object-src 'none'; base-uri 'none';");
    }
?>
PHP
CopyExpand

見ての通り、nonce を指定する設定である。‘object-src’、’base-uri’、’unsafe-inline’ などは指定していないと Lighthouse からご指導を賜る。
で、これも functions.php に書いておけば、その WordPress サイト全体で有効になるのではあるけれど、そのあとの各 javascript ファイルのロードやら、インラインスクリプトの対応などが忙しくなるので、header.php などへの記入にして、範囲を狭めて少しづつ対応していったほうが楽である。自分はそうした。

そう、“script-src” において “nonce” を設定すると、“nonce” を設定していない javascript ファイルもインラインで書いてある <script>も動かなくなるし、各タグにある、“onclick”“onchange” などは、“nonce” を設定したロードファイルやインラインスクリプトに改めて書き直さなければならなくなる。あと、“setTimeout”“setInterval” も、
setTimeout( "alert(Hello World!);", 500); のように書いてあるものは動かなくなる。

で、ちなみに上の 4行目の条件式は、管理画面とログインページにおいては設定しないためのもので、ログインページの場合は、グローバル変数の “pagenow” に’wp-login.php’ の文字列が入っていることを利用していますね。

php ob_start で wp_head, wp_footer で排出される <script> タグにまとめて nonce を付加する

実はこれはこの下の方にある、フックを使ってやる方法の後になって知ったやり方。ちゃんとこういうことができる関数が用意されているんだなぁ、と、ひと感心。
で、この ob_start というのは、通常、echo されたり <?php ?> タグの外で書かれたものは、その場ですぐに出力されてしまうのだけれど、その出力先をバッファに変更し、まとめてためておいて、それを変数に代入すればそのデータを編集するなど、扱えるようにできるというもの。

これを使えば、wp_head() wp_footer() から排出される全ての <script> タグにまとめて nonce を付加できる。と、いうことで以下のごとし。

<?php
	ob_start();// 出力データの溜め置き開始
	wp_head();
	$wp_head_str = ob_get_contents();// 溜め置いたデータを変数に代入
	ob_end_clean();// 溜め置き終わり

	echo str_replace( '<script', '<script nonce="' . CSPNONCE . '"', $wp_head_str );
?>
PHP
CopyExpand

ちなみに CSPNONCE は定数として functions.php で定義してあるのが前提で、であればこれはグローバルな値であり、どこからでもアクセスすることができる。
この方法だと、 wp_enqueue_script なんかでエンキューされた javascript ファイルをロードさせるための <script> タグ も、インラインのタグもどちらも適応できる。ただ、それぞれ wp_headwp_footer において変更する必要があるし、下にでてくる Akismet の場合なら、comment_form にも必要になってくるから、テンプレートがたくさんあるとそれだけ手間がかかることにはなる。

enqueue_script によって排出される script タグに nonce を付加する

ここからは、それぞれ個別に nonce を付加する方法を考えてみた。
まずは、テーマやプラグインなどで、wp_enqueue_script なんかでエンキューされ、wp_head() や wp_footer() から排出される javascript ファイルをロードさせるための <script> タグだけに nonce を付加させるかということ。
これに使えそうなフックはないかと探してみる。wp_enqueue_script がある wp-includes/functions.wp-scripts.php を見ていくと、wp_print_scripts() 関数の最後は return wp_scripts()->do_items( $handles ); となっている。class wp_script に何かありそうだと睨んで、wp-includes/class.wp-scripts.php に探す場所を変えて見ていくと、do_item() という関数の中に、script_loader_tag というフィルターがあった。これが使えそうだ。ためしにやってみると、このフィルター script_loader_tag にて目的を達成することができた。以下のごとし。

2023/2/15 追記:
この件、WordPress 6.2 Beta 1 においてエラーがでる。管理画面の投稿編集ページにおいて、何も表示されず真っ白な画面になってしまう。コンソールには post.php においてエラーが出るとのこと。その内容を見てみると、javascript のオブジェクト設定の部分において、この nonce の書き換えによりシンタックスエラーとなってしまうよう。

もともと管理画面においては、Content-Security-Policy を設定しないようにしているので、管理画面においては、この nonce を付加するフックも必要ないのである。ゆえに、三行目にその条件式を付加してある。

<?php
    function add_scripttag_nonce( $tag, $handel, $src ) {
        if ( ! is_admin() ) {
                $tag = str_replace( '<script', '<script nonce="' . CSPNONCE . '"', $tag );
        }
        return $tag;
    }
    add_filter( 'script_loader_tag', 'add_scripttag_nonce', 10, 3 );
?>
PHP
CopyExpand

ちなみに 上にも書いたけれど、CSPNONCE は定数として functions.php で定義してあるのが前提で、これはグローバルな値であり、どこからでもアクセスすることができる。
これにて、正規の手続きとして wp_enqueue_script で登録された script タグは全て nonce を付加することができるようになったわけではあるけれど、これで全てではない。

Akismet のインラインスクリプトの場合

テーマやプラグインによっては、インラインでのスクリプトを排出している場合もある。
Akismet はたぶん相当多くの人が使用しているプラグインだと思われるが、この Akismet にしてもインラインな <script> タグを排出していた。
この場合、comment_form フックを使っているわけなのだけれど、探してもここで排出される文字列に関しては、それを編集するようなフィルターフックは見当たらなかった。
ちなみに、Akismetcomment_form フックに 2つのフックがある。そういえばというか、ログイン状態でページを表示させたときに、もう一つ別のインラインスクリプトが存在していたので、たぶんそれなのだろう。追跡しなかったので確かではないけれど。複数あるとなるとこの下のような一度フックを remove してという方法ではなく、やはり上に書いた ob_start を使ったほうが手間もかからないし、これも確かではないけれど、処理の負荷も少ないような気がする。

この場合のフックは、comment_form なのだけれど、このフックは同名の関数、 comment_form() 関数の一番最後に書いてある。一番最後の処理として書いてあるので、横槍を入れることは出来ず、やはりこの comment_form() 関数に ob_start を使うことになる。といっても、別になんら違うことはなく同じようにするだけのことなのである。
普通、 comment_form() は、comments.php テンプレートに書いてある。

<?php
    ob_start();
    comment_form();
    $comment_form_str = ob_get_contents();
    ob_end_clean();

    echo str_replace( '<script', '<script nonce="' . CSPNONCE. '"', $comment_form_str );
?>
PHP
CopyExpand

さて、ob_start を使わない方法もあることにはある。
Akismet のファイルを調べていくと、フックの登録も肝心のファイルも、class.akismet.php の中に見つけることができる。そのスクリプトは、get_akismet_form_fields() 関数で生成され、comment_form フックに登録された output_custom_form_fields() 関数でエコーされてる。

と、いうことなら、一度、comment_form フックの登録を解除して get_akismet_form_fields() 関数 から文字列データを取得し nonce を付加する編集をして、再度、comment_form フックへ登録させればできるのではないかと。
登録してあるフックを消去する場合、どのフックにおいて remove_action すればいいのかがカギとなる。まだ登録される前にやっても、全く無意味だからである。

いろいろとやってみたところ、wp フックにて良い結果を得ることができた。

<?php
    // 例としてAkismet の実際のアクションフック登録
    // フックの指定はクラスの関数なので登録時と同じように配列にて指定する必要がある
	// add_action( 'comment_form', array( 'Akismet', 'output_custom_form_fields' ) );

    if ( class_exists( 'Akismet' ) ) {

        function remove_my_action(){

            remove_action( 'comment_form' , array( 'Akismet', 'output_custom_form_fields' ) );
        }
        add_action( 'wp', 'remove_my_action', 99);


        function akismet_output_form_fields() {
            $fields = Akismet::get_akismet_form_fields();

            $fields = str_replace( '<script', '<script nonce="' . CSPNONCE . '"', $fields );
            echo $fields;
        }
        add_action( 'comment_form', 'akismet_output_form_fields' );
    }
?>
PHP
CopyExpand

Emoji の場合

ローカルにおいてテストで使用していた主なデフォルトテーマは WordPress TwentyseventeenTwentytwentyone なのだけれど、排出される script タグで気がつくのは emoji
どれだけ多くの人がこの Emoji を使用しているのかわからないけれど、これは一応 WordPress においてのデフォルトで作動されるものであるからして、ちょっと気にかけてみた。

インラインスクリプトが wp_head で排出されているわけで、フックされているのは print_emoji_detection_script という関数。この関数はその関数名からも推測できるように、重複して書き出されることのないように判別するだけの関数で、本丸はこの関数から呼び出される _print_emoji_detecton_script である。

この関数を見てみると、排出される scritp タグの文字列は、<?php ?> タグの外に直に書かれている。 これを加工するにはどうすれば?と、探してみると phpob_start に行き着いたというわけ。
今までこういうことが必要な場面に遭遇することがなかったから知らなかったのだけれど、ちゃんとこういうことができるようになっているのだと、感心してしまった。ob_start に関しては、上に書いたとおり。

<?php
	function remove_my_action(){

		remove_action( 'wp_head', 'print_emoji_detection_script', 7 );

	}
	add_action( 'wp', 'remove_my_action', 99);

	function print_emoji_special_script() {
		ob_start();

		print_emoji_detection_script();
	
		$emoji_str = ob_get_contents();
		ob_end_clean();

		if ( $emoji_str ) {
			echo str_replace( '<script', '<script nonce="' . CSPNONCE . '"', $emoji_str );
		}
	}
	add_action( 'wp_head', 'print_emoji_special_script' );
?>
PHP
CopyExpand

remove_action のフックは Akismet と同様、wp で機能したので、両方必要なら一緒に書ける。

あとは、もしあれば、インラインで書いてある onclick とか onchange とかのイベントリスナーなんかを、ひたすらに掃除していくだけのこと。

ローカルにおいてテストで使用していたテーマ TwentyseventeenTwentytwentyone においては、そのどちらも wp_headwp_footer からいろいろと <script> タグが排出されている。
それら全てにおいてフックを加工してというのもとても手のかかることなので、その場合は、テンプレート側の wp_headwp_footerob_start の処理を付加していったほうが手間も時間もかからないし、処理速度の犠牲も少なくてすむと思う。

それとは関係のないことではあるけれど、ささいなことで、一つひっかかってしまって時間を無駄にしてしまったことがある。
それは、Chrome では出ないエラーが Firefox にて一つ出ていたこと。まぁ、このエラーを特定するのにひどく時間がかかってしまったというわけで。
しかし、こうしてみると Firefox のほうが信頼がおけるし、速度も速いと思うし、よほど使い勝手が良いと思ってしまう。なぜ Firefox のシェアはこんなにも少ないのだろう?まぁ、Lighthouse はないんだけれど。でも、そのときだけ Chrome を使えば良いのだし。
と、いうことで、Content-Security-Policy の設定をすることができたわけであるが、サイト表示の速度的影響もほとんどないようである。設定後の Lighthouse の値にほぼ変化は感じられない。そのうえ、Best Practices の項目の Not applicable の中に CSP は入っている。

Leave a Reply!

JavaScript is necessary to send a comment.
You can edit and delete your comment if you input a edit key.
Edit key is necessary for attesting you when you edit and delete it.
The tag of HTML cannot be used in comment.
When you comment for the first time, it is displayed after the approval of the administrator.
Because I cannot speak English so much, it takes time to answer.
Required fields are marked *.

※Please enter more than 5 characters only alphabets.
※Edit or delete are possible for 2000 days after approval.

*

♠Simplistic Comment User Editable v4.0

♠When visitors leave comments on the site this site collect the data shown in the comments form, and also the visitor’s IP address and browser user agent string to help spam detection.
♠This site does not use cookie when visitors leave comments and commenter edit comment.
♠This site uses Akismet to reduce spam. Learn how your comment data is processed.

Comments feed

Trackback URL : https://strix.main.jp/wp-trackback.php?p=168523

Sanbanse Funabashi
2010.10.24 sunrise

Top

スクロールさせるか画像をクリックすると元に戻ります。