Content Security Policy Lv3 (CSP3)でhashによりスクリプトを許可する

整理している人
©いらすとや.

前回、個人ブログであってもCSPを付けたいということを書いた。とっつきにくいので手を出し難いかもしれないけど「難しい」と尻込みしないで思い切って飛び込んでみれば実はそんなに難しくはないかも。専門家の人には怒られるかもしれないけどCSP3の 'strict-dynamic' さえ使えば楽勝よと思っておいて良いかも。

hash(ハッシュ)の求め方

例えば1行だけのJavascriptがページにあるとする。このスクリプトのhashを求める。(↓)

<script>alert('test');</script>

<script></script> は含めない。hashを求める対象はこの例では「alert('test');」の部分。

$ echo -n "alert('test');" | openssl sha256 -binary | openssl base64
GAF48QOoxRvu0gZAmQivUdJPyBacqznBAXwnkfpmQX4=       ←これが求められたhash

echoコマンドは普通は最後に改行がつくので改行を付けない「-n」を引数に付ける。
求めたhashの文字列の前に「sha256-」を付けて全体の前後に「'」を付ける。
'sha256-GAF48QOoxRvu0gZAmQivUdJPyBacqznBAXwnkfpmQX4='
これで1つ出来上がり。1行の短いスクリプトであれば難しいことはなくてこれで。

複数行、シングルクォート「'」,ダブルクォートが「"」が入り交じるスクリプトは面倒なのでそれだけを1つのファイルに書き込む。次が元のスクリプトとする。

<script>
var a = 'test';
alert( a );
</script>

先にも書いたが<script></script> は含めないので消す。ただし、ハッシュを求める際は改行も対象に含まれる
この例では、<script>var の間に改行、alert( a );</script>の間に改行があるのでその改行は残す。(これが注意)

                 改行だけの行(空行)
var a = 'test';
alert( a );           ;の後で改行
                 ←空行

このようにして今回はファイル(hash1.txt)に保存。
なお、vimのようにファイルの最後に(編集画面では)見えない改行が勝手につくエディタを使う場合はその勝手に付く最後の空行に注意。これ結構やらかして悩まされそう。vimの場合、:set bin noeol して :wqでファイル最後の改行無しで保存できる。(1行しかないと変な挙動で空ファイルになったりファイルが作成されないかも)

$ cat hash1.txt | openssl sha256 -binary | openssl base64
lpEdviF1nW++20sB72uy8WJPj9AfL8JDneneL6N1etU=     ←これが求められたhash

先の例と同様に求めたhashの文字列の前に「sha256-」を付けて全体の前後に「'」を付ける。
'sha256-lpEdviF1nW++20sB72uy8WJPj9AfL8JDneneL6N1etU='
これで2つめが出来上がり。
実際に使うことが多いのはこちらのやり方かと。

Report URIのウェブツールでハッシュ生成
ターミナルとかコマンドラインとか全然しらないし怖くて触りたくないという人向けにオンラインツール https://report-uri.com/home/hash が便利。こちらも<script></script> の前後の改行があるなら空行が必要。なのでページの <script> の後の改行と </script> の前の改行なんて付けないのが無難なのかも。

さらにカンタンなのは、(以下)
CSPヘッダを作成する前に仮で次のようなCSPヘッダをウェブサーバで設定する。

Content-Security-Policy: script-src 'strict-dynamic' https: http:;
ウェブサーバがNginxならこんな指定(↓)
add_header 'Content-Security-Policy' "script-src 'strict-dynamic' https: http:;";

ウェブサーバに設定を読み込ませてこのCSPヘッダを有効化する。その状態で次。

Report URIのウェブツールでハッシュ生成
ブラウザを開く。
重要: 拡張機能は全て無効化するか、シークレットウィンドウでCSPを設定するウェブサイトのページを表示する。Chromeブラウザであれば[F12]を押してデベロッパツールを開き、「コンソール」タブを選択する。
CSPのエラーが表示されるが、親切にもページ内の全てのスクリプトのハッシュが表示されるのでそれをコピーしてCSPヘッダに追加する(全て)

やり方としては最も簡単なのだが、どのハッシュがどのスクリプトのなのかが判らないので困るかも。

CSPヘッダを作る

1
2
3
4
5
6
7
8
Content-Security-Policy: script-src 'sha256-GAF48QOoxRvu0gZAmQivUdJPyBacqznBAXwnkfpmQX4='
                                    'sha256-lpEdviF1nW++20sB72uy8WJPj9AfL8JDneneL6N1etU='
                                    'strict-dynamic'
                                    'unsafe-inline'
                                    https:
                                    http:;
                         object-src 'none';
                         base-uri 'none';

'strict-dynamic' は付けるのがオススメ。これがあることで許可したスクリプト(今回ハッシュを求めたスクリプト)から別のスクリプトが呼ばれる場合にそのスクリプトを追加する必要がないのでカンタン。逆に、つけないとAdSenseなどの外部の広告サービスを使用する場合にCSPの許可リストが破綻するかも。(クリック広告サービスではAdSenseは実はマシな方。)
'unsafe-inline' は古いブラウザのために付けておく。CSP2,CSP3を理解するブラウザはこの指定を無視するので大丈夫。(デベロッパツールには警告として出るが害はない) むしろ、最近のPageSpeed Insightsでは'unsafe-inline'無しにしてると互換性のために'unsafe-inline'を付けろと言ってくるくらい。
出来たら1行にしてウェブサーバの設定に加える。

例: Nginx
add_header 'Content-Security-Policy' "script-src 'sha256-vD3/JXDdBKKMffxw2mAt10e7VpBqnfXak1o+9jzbxgs=' 'sha256-lpEdviF1nW++20sB72uy8WJPj9AfL8JDneneL6N1etU=' 'strict-dynamic' 'unsafe-inline' https: http:;object-src 'none';base-uri 'none';";

前回書いたが、CSP2では外部スクリプトをハッシュで許可するのはできなくて、CSP3ではそれができるけどブラウザの対応がまだ不安なので採用には躊躇する?また、外部スクリプトをハッシュで許可というのはその外部スクリプトに変更が加えられると許可が外れてしまうので怖い。バージョン毎に違うURLで提供されるようなライブラリなら大丈夫だろうけど。
この2つの問題に対応するために、外部スクリプトを内部スクリプト(動的スクリプト)にして読み込むのが「がとらぼ」の中の人としてはお薦めの方法。(以下)

例えば外部スクリプトの https://orange.example.com/banana.js を利用したいとする。
普通であれば <script src="https://orange.example.com/banana.js"></script> のようになる。

これを内部スクリプトから読むようにする。

1
2
3
4
5
<script>
var scr = document.createElement("script");
scr.src = "https://orange.example.com/banana.js";
document.head.appendChild(scr);
</script>
ハッシュは上と同様の求め方で 'sha256-OA/LscJiJ8EYhoLVBpK5Pos3J251HJhvYg5Wvpe477Q='

AdSenseのコードにハッシュを付けて許可する

Google AdSenseなら元はこのようなコード。(最近の)
1
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-0000000000000000" crossorigin="anonymous"></script>

これは外部スクリプトなのでハッシュを付けられない。(CSP3の外部スクリプトのハッシュ対応ブラウザを除く)
そこで動的スクリプトに変形してハッシュ対応にする。

1
2
3
4
5
6
<script>
var scr = document.createElement("script");
scr.src = "//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-0000000000000000";
scr.crossorigin = 'anonymous';
document.head.appendChild(scr);
</script>

動的スクリプトは最初から非同期なのでasync不要。この例では最後のappendChild()はGoogleさんのドキュメントどおりにdocument.head(HTMLヘッダ <head>〜</head>)の子要素として付けているが、HTMLボディ(<body>〜</body>)の子要素にしたければdocument.bodyにする。個人的にはHTMLヘッダにスクリプトを置くのが嫌いなのでbody派です。

このように変換するならついでにAdSenseの遅延表示を導入するのもアリです。遅延表示させるために動的スクリプトにするのでそれからハッシュを求めてやれば良いことになります。AdSenseの遅延表示についてはググってください。

AdSenseの個別の広告ユニットでは以下のようなコードを使います。この内、Javascriptは最後の3行です。&;tscript&gtlの次の改行とスクリプト行の行頭のスペース、スクリプト行の最後に改行があるのでお忘れなく。

<ins class="adsbygoogle"
	style="display:block"
	data-ad-client="ca-pub-0000000000000000"
	data-ad-slot="0000000000"
	data-ad-format="auto"
	data-full-width-responsive="true"></ins>
<script>
	(adsbygoogle = window.adsbygoogle || []).push({});
</script>

同様にハッシュを求めてCSPヘッダに追加します。広告ユニットが複数あって広告ユニットの種類が違ったとしてもJavascript部分は同じ筈なので1つハッシュを作るだけです。
なので、AdSenseに限れば基本的にはpagead2.googlesyndication.com/pagead/js/adsbygoogle.jsを読み込むスクリプトと広告ユニットの「(adsbygoogle = window.adsbygoogle || []).push({});」の2つのハッシュだけで済むということです。CSP1の個別許可だとAdSense用だけでも多くのホストを許可しなくてはならないのが2つだけというのは素晴らしく気分が良いというものです。

Google Analyticsのコードを動的スクリプトにしてハッシュを付ける

1
2
3
4
5
6
7
8
<script async src="https://www.googletagmanager.com/gtag/js?id=G-0000000000"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-0000000000');
</script>
仮に、Analytics用のコードがこのようなものだとします。2つのスクリプトで、1行目は外部スクリプトを読み込むもの、2〜8行目は内部スクリプトです。 1行目のスクリプトを動的スクリプトに書き換えてハッシュを求め、2〜8行目のスクリプトの方もハッシュを求めます。つまりこの例では2つのハッシュを作成してCSPヘッダに追加することになります。先のAdSenseも使っているならAdSenseとAnalyticsで4つのハッシュです。

作成したCSPヘッダをPHPで出力する

基本的にはApacheやNginxのようなウェブサーバでCSPヘッダを出力する方が良いと思いますが、レンタルサーバによってはその辺りは触らせて貰えないかもしれません。PHPが使えるサーバならPHPで出力させることができます。

<?php
        header("Content-Security-Policy: script-src 'sha256-GAF48QOoxRvu0gZAmQivUdJPyBacqznBAXwnkfpmQX4=' 'sha256-lpEdviF1nW++20sB72uy8WJPj9AfL8JDneneL6N1etU=' 'sha256-BYZOgvpTTjvdOWhTv/ssZ9j0ujjYZVRqiMDllmV8ZlQ=' 'sha256-ZbBokixf9ZfhHkdE8RY5euZnQTEGGYGODZhRpq917sM=' 'strict-dynamic' https: http:;");
?>

見ればわかりますが header("出力させたいHTTPヘッダの文字列"); だけです。

WORDPRESS/wp-content/themes/使用中のテーマ/functions.php (挿入)
1
2
3
4
5
6
7
8
function response_header_insert() {
    if($response_headers = get_post_meta(get_the_ID(), 'response_header', false)){
        foreach($response_headers as $header_code){
            @header( $header_code );
        }
    }
}
add_action( 'send_headers', 'response_header_insert', 10, 0 );

WordPressで任意のHTTPレスポンスヘッダを記事のカスタムフィールドで設定できるようにするコードを作ってみました。任意のHTTPスレポンスヘッダなのでCSP専用ではありません。投稿/固定記事のカスタムフィールドの「名前」の欄、初めてHTTPレスポンスを登録する場合は「新規追加」リンクを押して名前欄に「response_header」と入力します。一度登録すると次回以降は「名前」のドロップダウンに「response_header」があるのでそれを選択します。
値のテキストボックスに「Content-Security-Policy: 文字列」を入力して記事を保存します。「文字列」の部分は上で作成したCSPの設定です。
なお、手抜きのコードなので他にHTTPヘッダを出力する機能があるとその機能が出力したHTTPヘッダを壊す(無くす)可能性があります。そのまま使おうとせずにご自身で問題ないものにしてください。

カスタムヘッダなんてものではなく、単にテーマ内のsend_headers用のアクションの中で直接header("CSPヘッダの文字列");でも良さそうです。その際はis_admin()で分岐し、管理者パネルではCSPヘッダを出力しないようにする方が無難です。

なお、ページ表示高速化のためにページキャッシュの仕組みを導入している場合、PHPでHTTPレスポンスヘッダを出力しても、ページキャッシュではそれは出力されません。つまり、キャッシュがヒットせずに動的ページ作成が行われた場合はHTTPレスポンスヘッダがあって、キャッシュがヒットしてそれが表示された場合はHTTPレスポンスヘッダがありません。これはウェブサイトオーナーの意図する動作ではない筈です。ページキャッシュを使用しているサイトでは素直にウェブサーバでHTTPレスポンスヘッダを出力するのが無難です。(または次)

HTMLヘッダのmetaタグでCSPを出力する

<meta http-equiv="Content-Security-Policy" content="文字列">

content="文字列"の部分にCSPの設定を入れるだけです。これも1行で書きます。
個人的にはHTMLヘッダでCSPを設定するのは好きではありませんが、ウェブサーバの自由がない、ページキャッシュを使用しているなどの理由でどうしても使わざるを得ないということがあるかもしれません。

次回はハッシュではなく使い捨て合言葉nonse(ナンス)を使う場合というのをやりたいと思っているけど、いまのところ自身で使ってないので暫くかかるかも。「悪いインターネット」で暫く試行錯誤したいと思います。

Content Security Policy (CSP) の振り返り

フィッシング詐欺
©いらすとや.

Content Security Policy (CSP)は、ウェブサイト側で定めたポリシーに従ってブラウザがコンテンツ(の要素)を表示する/表示しないまたは外部サイトにそのサイトの要素を表示させる/表示させないことによって閲覧者を守るセキュリティ。ウェブサイトに脆弱性があるなどしてクロスサイト・スクリプティングを仕掛けられようとしても閲覧者が守られる可能性はあるが、ウェブサーバを守るセキュリティということではないので手を付けないウェブサイトオーナーさんも多い。でも、閲覧者を守るというのは結局はウェブサイト(運営母体)のブランドも守ることになるのでしっかり考慮して設定したいところ。これは企業はもちろん個人ブログの類であっても。

Content Security Policy Level 1 (CSP1) (オワコン)

HTTPレスポンスヘッダ
Content-Security-Policy: default-src 'self';
                         script-src trustedscripts.example.com 'unsafe-inline' 'unsafe-eval';
                         object-src media1.example.com media2.example.com *.cdn.example.com;
                         frame-src frame1.example.com;
                         font-src font1.example.com font2.example.com *.cdn.example.com;
                         img-src *;

上は見易いように改行して複数行になっているが、実際には改行しないで1行。

以前よく見かけたというか現在もよく見かけるCSPの指定。ページに読み込む許可を与えるコンテンツの種類毎にself(ウェブのホスト自身)と外部のホスト/ドメイン(ワイルドカード可)を指定するので一見とっつきやすいのだけれど外部のサービスを利用するとどのホスト/ドメインから読み込むかしっかり調べてリストアップしなければならないが、読み込み許可しなければならないホスト/ドメインが増えたり変更になったときに面倒というか正直追いきれないことがある。結果的に * (全て)のようなユルユルな指定をしてCSPを設定する意味がなくなってしまうことがよくある。ウェブ広告を利用する/YouTubeやGoogleMapsの地図を貼り付けるなど外部サービスを利用するとそうなりやすいというかなる。
もちろん、全ての要素(画像/Javascript/CSS/フレームなど)が自身のウェブサイトにあるとかウェブフォントを外部1箇所から取るだけとかであれば全く面倒なく強固な指定ができる。

Content Security Policy Level 2 (CSP2)

HTTPレスポンスヘッダ
Content-Security-Policy: script-src 'self'
                                    https://example.com
                                    'nonce-ナンス文字列1'
                                    'nonce-ナンス文字列2';
                         base-uri 'self';
ページの中
<script nonce="ナンス文字列1" src="https://hoge.example.com/hage.js"></script>
<script nonce="ナンス文字列2" src="https://foo.example.com/bar.js"></script>
hash(ハッシュ)指定の例
HTTPレスポンスヘッダ
Content-Security-Policy: script-src 'self'
                                    https://example.com
                                    'sha256-スクリプトのハッシュ1'
                                    'sha256-スクリプトのハッシュ2';
                         base-uri 'self';
ページの中
<script>The quick brown fox jumps over the lazy dog.</script>   The〜dog.のハッシュを求める→ハッシュ1
<script>The rain in Spain stays mainly in the plain.</script>   The〜plain.のハッシュを求める→ハッシュ2

レベル1から大きく変わっている。
使用するディレクティブ (script-src, frame-src, font-srcのようやつ)が追加、変更(+非推奨化)されている。整理された筈なのに解りにくくなってるのがこれ。
ページ表示するために他所の要素(画像/スクリプト/フォントなど)を使用することを許可/不許可するのではなく、自身のサイトを外部のサイトに使わせる/使わせないための指定が追加。(frame-ancestors)
nonceやhashで個別に要素を許可できる。
など、いろいろあるのでご確認ください。

CSP Lv1より難しくなった感はある。特にnonceとhashはとっつきにくい。

Content Security Policy Level 3 (CSP3) 草案

nonse(ナンス)指定の例
HTTPレスポンスヘッダ
Content-Security-Policy: script-src 'strict-dynamic'
                                    'nonce-ナンス文字列1'
                                    'nonce-ナンス文字列2';
                         base-uri 'self';
ページの中
<script nonce="ナンス文字列1" src="https://hoge.example.com/hage.js"></script>
<script nonce="ナンス文字列2" src="https://foo.example.com/bar.js"></script>
hash(ハッシュ)指定の例
HTTPレスポンスヘッダ
Content-Security-Policy: script-src 'strict-dynamic'
                                    'sha256-スクリプトのハッシュ1'
                                    'sha256-スクリプトのハッシュ2';
                         base-uri 'self';
ページの中
<script>The quick brown fox jumps over the lazy dog.</script>  The〜dog.のハッシュを求める→ハッシュ1
<script>The rain in Spain stays mainly in the plain.</script>  The〜plain.のハッシュを求める→ハッシュ2

CSP2で追加されたchild-srcディレクティブはframe-srcを代替するものの筈でしたがchild-srcを使わなければ引き続きframe-srcで良いらしい。worker-srcも同様?
CSP2で追加になったnonceとhashは仕組みとしては手間がかかる。しかし'strict-dynamic'が追加されたおかげでこれが生きるものになった。 というのも、例えば許可した(信頼した)Javascriptが別の外部要素を呼び出した際、それを追加許可しなくて済むので本当にラクになった。

CSP2で追加になったhashはインラインスクリプトにしか使えない。これはページ内の <script>hogehoge</script> には使えるけど <script src="https://example.com/hagehage.js"></script> には使えない、つまり外部スクリプトには使えない。これがCSP3ではhashが外部スクリプトにも使えるようになる。ただし、書き方が少し違うらしいのとブラウザの対応がまだ追いついていないかも。(つまり「まだ」メジャーブラウザでも使えないかも)

外部スクリプトをhashで許可できるのは一見喜ばしいことかもしれないが、(知らない内に)その外部スクリプトに変更が加わると利用できなくなるので実は筋悪ではないかと思う。個人的には外部スクリプトを内部スクリプトから呼び出すようにして、その内部スクリプトのハッシュで許可するのが良いかと思う。

次回、実際にCSP3を設定する。
その際、AdSenseやGoogle Analyticsなど外部サービスを利用する場合に対応した実用的なものにする予定。

Up