近年はXSS対策などのためサーバ上でCSPヘッダを付加することが増えている。詳しいことはリンク先に書かれているが,簡単に言うとHTTPヘッダでjavascriptやStylesheetを読み込んで実行する対象を制限することができる。これによって,不正なスクリプトが実行されることを防止しようというものだ。
ところが,Facebookと連携させようとすると,CSPが邪魔をしてしまう。Facebookのsdkはjavascriptの中で更に別のjavascriptを読込み,動的にscriptタグをDOM内に追加するのである。CSPヘッダのscript-srcに’unsafe-inline’を指定することで実行できるのであるが,これではXSSに対して無防備となる。インラインのjavascriptには毎回nonce属性にランダムな文字列を生成してセットし,CSPヘッダにも’nonce-{設定したnonce値}’という文字列を設定した方がよい。ただし,Facebookのようにjavascript内で動的に生成されるscriptタグにnonce属性は設定されていない。そのためにブラウザで読み込まなくなってしまうのである。

CSPによりJavascriptがブロックされている
動かすためには,動的に追加されるscriptタグにnonceを設定する必要がある。facebookのjavascriptを書き換えられればよいのであるが,それはできない。どうするか迷った末に,scriptタグを追加する関数を書き換えてみることにした。
htmlのDOMからheadノードを取得して,そのappendChild()メソッドを書き換えて,子要素を追加する際に追加される子要素にnonce属性を追加してしまおうという作戦である。次のようにhead要素を取得(1行目)して,そのappendChildメソッドの参照を取得ておく(2行目)。そして,head要素のappendChildメソッドに新しくfunctionを定義する(3-6行目)。このfunctionの中では子要素にsetAttributeメソッドを使って”nonce”属性を設定する。その後,2行目で取得しておいた元々のappendChildメソッドの参照を使って,元の関数を呼び出す。
head = document.getElementsByTagName('head')[0];
_appendChild = head.appendChild;
head.appendChild = function(child) {
child.setAttribute('nonce', "<?php echo $nonce; ?>");
_appendChild.apply(this, arguments);
}
このJavascriptを組み込んで再度Webブラウザで読み込むと,ブロックされずにJavascriptが実行された。


実験に用いたソースファイル(index.php)
<?php
function getNonce() {
return base64_encode(openssl_random_pseudo_bytes(20));
}
function createCSPHeader($nonce) {
$nonces = array(
'default-src' => "'self'",
'img-src' => "'self' data:",
'script-src' => "'unsafe-eval' 'strict-dynamic' 'nonce-".$nonce."'" ,
'style-src' => "'nonce-".$nonce."'"
);
$csp = 'Content-Security-Policy: ';
$delimiter = '';
foreach($nonces as $key => $value) {
$csp = $csp.$delimiter.$key.' '.$value;
$delimiter = ';';
}
return $csp;
}
$nonce = getNonce();
$csp_header = createCSPHeader($nonce);
header($csp_header);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<title>PHP TEST</title>
<style type="text/css" nonce="<?php echo $nonce ?>">
.container {
background-color: #F3F3F3;
width: 100%;
height; 100%;
}
</style>
<script nonce="<?php echo $nonce; ?>">
window.fbAsyncInit = function() {
FB.init({
appId : 'YOUR_APPLICATION_ID',
cookie : true,
oauth : true,
version : 'v2.9'
});
}
</script>
<script nonce="<?php echo $nonce; ?>">
head = document.getElementsByTagName('head')[0];
_appendChild = head.appendChild;
head.appendChild = function(child) {
child.setAttribute('nonce', "<?php echo $nonce; ?>");
_appendChild.apply(this, arguments);
}
</script>
</head>
<body>
<div class="container">
<h1>Hello World.</h1>
<p>Nonce: <?php echo $nonce; ?></p>
<p><?php echo $csp_header; ?></p>
</div>
</body>
</html>