2017-01-26

tobijibu

DokuWikiのContent-Security-Policy対応方法

前回、pjaxを設定しました。サンプルテンプレートに記載されたCSP(Content-Security-Policy)の対応を見送りましたので、今回はCSPを導入します。なお、CSPの設定が不要であれば、この記事の対応は不要です。環境に併せて判断してください。

CSPとは、ページを表示する時に使うリソース(素材)を指定したサイトからだけ読み込みを許可するルールです。CSPを利用することによって、悪意ある外部のスクリプトの実行を阻止したり、インラインスクリプトによるクロスサイトスクリプティング(XSS)の脅威を軽減することができます。つまり、サイトの安全性を担保するためのルールづくりです。

DokuWikiであればPHPからheader使ってルールを送信する方法か、htmlの<head>内にルールを指定することになります。

今回の説明では、前回修正したテンプレートをさらに修正するという方法で説明していきます。

今回説明で利用するソースはこちらにあります。

CSPを定義する

DokuWikiの場合、インラインスクリプトがいくつかあるので、それを個別に対処していく必要があります。テンプレートによっては対応不要の場合があるかもしれません。

CSPは下記のように定義しました。

default-src
    'self';
img-src
    'self'
    data:
    http://www.dokuwiki.org
    http://www.gravatar.com
    http://www.google-analytics.com;
style-src
    'self';
script-src
    'self'
    'nonce-".$NONCE."'
    http://www.google-analytics.com;
child-src
    https://youtube.com;

PHPからheaderで送信するには1行でなければならないので、下記のようになります。

header("Content-Security-Policy: default-src 'self'; img-src 'self' data: http://www.dokuwiki.org http://www.gravatar.com http://www.google-analytics.com; style-src 'self'; script-src 'self' 'nonce-".$NONCE."' http://www.google-analytics.com; child-src https://youtube.com;");

設定の内容について一つ一つ解説します。

img-src

画像の取得許可のルールを指定します。

一部の画像がdata:で指定されているので、data:を追加指定します。他のサイトから画像を直リンクで取得している場合には画像のドメインを指定します。最低限、下記の3サイトは指定しておくと良いです。

http://www.dokuwiki.org         → DokuWikiサイトの画像(管理画面のプラグイン一覧で使う)
http://www.gravatar.com         → gravatarの画像(管理画面のプラグイン一覧で使う。開発者アバターアイコン)
http://www.google-analytics.com → google Analyticsのスクリプトで利用する画像

style-src

CSSの取得許可のルールを指定します。

自サイトのスタイルのみを許可しています。ただし、これを指定しただけではCSP違反が発生します。

実は、あるjsで<style>~</style>が生成されているためです。対処としては2つあります。サイトの特性にもよりますが、CSP導入の意味を踏まえるならば後者をお勧めします。

1. 'unsafe-inline'を入れる

unsafe-inlineを入れることで、スタイルのインライン指定を許可します。こうすることで、jsで生成された<style>もCSP違反になりません。ただし、CSSを使った高度な攻撃手法もあるようなので、油断はできません。

記事内のタグにstyleを埋め込んでいる場合は、実質こちらの指定方法しか無いといえます。

2. styling pluginを無効化

<style>~</style>を生成しているプラグインは、"styling plugin"です。このプラグインを無効化することによってCSP違反を回避できます。

ただし、管理画面上で「テンプレートスタイルの設定」ができなくなります。テンプレートスタイルを変更する場合は直接CSSを編集することになるでしょう。

script-src

JavaScriptの実行許可のルールを指定します。

一部のスクリプトがインラインで指定されており、外部化しづらい仕組みになっています。そのため、nonce-*というプロパティを使います。nonce-*の設定は後ほど説明します。

child-src

iframe読み込むコンテンツのルールを指定します。

iframeは以前frame-srcで指定していましたが、frame-srcが非推奨になったため、child-srcを指定します。

テンプレート修正

CSPの定義できたので、テンプレートを修正していきましょう。

まずは先ほど作成したheaderを挿入します。元ソースのll.14-15の間に挿入します。

header('X-UA-Compatible: IE=edge,chrome=1');
/**
 * $_SERVER['HTTP_X_PJAX']でpjaxアクセスかどうかを判別します。
 * headerにはContent-Security-Policyを設定します。これはXSS等の攻撃を軽減するための設定です。
 * コンテンツ取得する際に任意のドメインからのみ許可するという設定をします。
 * ここでは、自サイトのコンテンツ、googleのapi、youtubeを許可しています。
 */
if ('true'!=$_SERVER['HTTP_X_PJAX']){
    header("Content-Security-Policy: default-src 'self'; img-src 'self' data: http://www.dokuwiki.org http://www.gravatar.com http://www.google-analytics.com; style-src 'self'; script-src 'self' 'nonce-".$NONCE."' http://www.google-analytics.com; child-src https://youtube.com;");
}
$hasSidebar = page_findnearest($conf['sidebar']);
$showSidebar = $hasSidebar && ($ACT=='show');

jsのインライン利用不可とするので、インラインで記述しているjsを外部化します。

テンプレート内の<head>タグにある下記の処理をinit.jsとして保存し、外部jsとして読み込みます。init.jsmain.phpと同階層に設置してください。

<title><?php tpl_pagetitle() ?> [<?php echo strip_tags($conf['title']) ?>]</title>
<?php
/**
 * インラインのjsを無くします
 * 以下の処理を別ファイルに保存し、そのファイルを外部jsとして読み込みます。
 */
//init.jsを作り以下の処理をコピーする
//<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>
?>
<script src="<?php print DOKU_TPL . 'init.js'; ?>" defer></script>
(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement);

続いて、tpl_metaheadersで呼ばれているインラインスクリプトの対応です。./inc/template.phptpl_metaheadersメソッドではインラインスクリプトを生成しています。この部分でもCSP違反が発生します。

これを回避する方法として、先ほど出てきたnonceプロパティを追加して対応します。

ところで、nonceとはなんだろう?という疑問が浮かぶかもしれません。nonceとは"number used once"の略で、"一度しか使えない番号"という意味です。

アクセス毎にユニークなIDを生成し、headerのCSPとscriptに共通のIDを付与することで、"サイト作成者が埋め込んだスクリプトであること"を示すことができる仕組みです。

インラインスクリプトであったとしても、nonceプロパティを使うことで、サイト作成者のスクリプトであることが確約できるため、安全であることをブラウザに知らせることができます。

まず、先ほど追加したpjax判定の内部、header処理の前でheaderscriptに指定するIDを生成します。

不本意ではありますが、globalキーワードを使っています。DokuWikiでは他の定義や処理のあらゆる箇所で使われているので良しとします。一応、他の変数と合わせて大文字で定義しました。


header('X-UA-Compatible: IE=edge,chrome=1');
/**
 * $_SERVER['HTTP_X_PJAX']でpjaxアクセスかどうかを判別します。
 * headerにはContent-Security-Policyを設定します。これはXSS等の攻撃を軽減するための設定です。
 * コンテンツ取得する際に任意のドメインからのみ許可するという設定をします。
 * ここでは、自サイトのコンテンツ、googleのapi、youtubeを許可しています。
 */
if ('true'!=$_SERVER['HTTP_X_PJAX']){
    /* really the template should expose control over the policy via the admin page....*/
    global $NONCE;
    $NONCE = bin2hex(openssl_random_pseudo_bytes(16));
    header("Content-Security-Policy: default-src 'self'; img-src 'self' data: http://www.dokuwiki.org http://www.gravatar.com http://www.google-analytics.com; style-src 'self'; script-src 'self' 'nonce-".$NONCE."' http://www.google-analytics.com; child-src https://youtube.com;");
}
$hasSidebar = page_findnearest($conf['sidebar']);
$showSidebar = $hasSidebar && ($ACT=='show');

続いて、main.phpと同じ階層にphpファイルを作成します。ファイル名は自由ですが、ここではmy_template.phpとしましょう。

次に、./inc/template.phptpl_metaheadersメソッドをコピーしてmy_template.phpに貼り付けます。メソッド名はなんでも構いませんが、ここではmy_tpl_metaheadesとしましょう。

生成したmy_tpl_metaheadesメソッドの内容を少し書き換えます。メソッドの引数に$NONCEを追加します。そして、132行目の配列の末尾に, 'nonce' => $NONCEを追加します。

<?php
function my_tpl_metaheaders($alt = true, $NONCE = null) {
    global $ID;
    global $REV;
$script .= 'var JSINFO = '.$json->encode($JSINFO).';';
$head['script'][] = array('type'=> 'text/javascript', '_data'=> $script, 'nonce' => $NONCE);
// load external javascript
$head['script'][] = array(
    'type'=> 'text/javascript', 'charset'=> 'utf-8', '_data'=> '',
    'src' => DOKU_BASE.'lib/exe/js.php'.'?t='.rawurlencode($conf['template']).'&tseed='.$tseed
);

そして、main.phptpl_metaheaders()を呼び出している箇所(元ソースl.32)をmy_tpl_metaheaders(true, $NONCE)に書き換えます。

<script src="<?php print DOKU_TPL . 'init.js'; ?>" defer></script>
<?php my_tpl_metaheaders(true, $NONCE) ?>
<meta name="viewport" content="width=device-width,initial-scale=1" />

googleAnalyticsの対応

googleAnalyticsを利用している場合、プラグインを使っているかどうかで対応が変わります。

プラグインを利用していない場合

先ほどのinit.jsと同じく、googleAnalyticsのタグをそのまま外部スクリプト化するだけです。

プラグインを利用している場合

googleAnalyticsプラグインに手を加えることになります。

./lib/plugins/googleanalytics/action.php内の_addHeadersを編集します。

30行目のglobal $INFO;の下にglobal $NONCE;を追記します。

function _addHeaders (&$event, $param) {
    global $INFO;
    global $NONCE;
    if(!$this->getConf('GAID')) return;

続いて、処理内では$event->data["script"][]に配列を代入していますが、その配列の末尾に"nonce" => $NONCE,を追記します。2箇所ありますので、どちらにも追記してください。これでgoogleAnalyticsの対応が完了です。

なお、2009年以降更新が無いようなので大きく変わることが無いという想定です。処理もシンプルですし。プラグインのバージョンアップに応じて変更してください。

$event->data["script"][] = array (
  "type" => "text/javascript",
  "_data" => "
    var gaJsHost = ((\"https:\" == document.location.protocol) ? \"https://ssl.\" : \"http://www.\");
    document.write(unescape(\"%3Cscript src='\" + gaJsHost + \"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E\"));
  ",
  "nonce" => $NONCE,
);
$event->data["script"][] = array (
  "type" => "text/javascript",
  "_data" => "
    var pageTracker = _gat._getTracker(\"".$this->getConf('GAID')."\");
    pageTracker._initData();
    pageTracker._trackPageview();
  ",
  "nonce" => $NONCE,
);

対応完了

これでユーザーが表示する画面のCSPの対応が完了です。お疲れ様でした。

管理画面では、一部インラインstyleが利用されている箇所があります。今回の対応だけでは管理画面全てのCSP違反を対処出来ていません。管理画面は特定ユーザーのみが利用するという前提で対応は見送っています。全て直したい場合はやはりstyle-srcunsafe-inlineを指定するか、1つ1つ根気よく確認してCSSを外部化するしかなさそうです。