情報セキュリティ

CSPヘッダとFacebook SDK

近年は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>