PHPでCSP(Content Security Policy)を導入してXSS対策を強化してみよう

PHPで簡単にCSPを導入するためのライブラリを作成してみました。

GitHubリポジトリのスクリーンショット

既存サイトへの影響を最小限にしてCSPが導入できることを目的としています。

基本的にCSP nonce-sourceを使い、nonceのないscriptタグは実行しないようにすることでXSS対策を強化します。

このライブラリの仕様としては、CSP nonce-sourceに対応していると思われる指定ブラウザに対してのみCSPヘッダを出力します。現状、ChromeとFirefoxのみが指定されています。

なお、CSP nonce-sourceに対応したChromeのバージョンがわからないので、確認できたバージョン37以上としてます。

CSPについて

CSPについてよく知らない方は以下のスライドなどをご覧下さい。

このライブラリの使い方

Composerで「kenjis/csp」をインストールするか、GitHubからcloneするなり、Zipファイルを取得してください。

$ git clone https://github.com/kenjis/php-csp-nonce-source.git
$ cd php-csp-nonce-source
$ composer install

使い方のサンプルは以下の通りです。

index.php

<?php
require __DIR__ . '/bootstrap.php';
Csp::sendHeader();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sample of CSP nonce-source</title>
</head>
<body>

<script type="text/javascript" nonce="<?= Csp::getNonce() ?>">
    alert('This works!');
</script>

<script type="text/javascript">
    alert('This does not work!');
</script>

</body>
</html>

Csp::sendHeader();でCSPヘッダが出力されます。

標準では

Content-Security-Policy: script-src 'nonce-$RANDOM'; report-uri /csp-report.php

が出力されますので、nonce属性のないscriptタグは(CSP nonce-source対応ブラウザでは)実行されなくなります。

Csp::getNonce();でnonceの値を取得します。

既存サイトでは、CSPヘッダが出力されるようにして、すべてのscriptタグにnonce属性を追加すれば完了です。

CSP違反レポートが、csp-report.logとして記録されますので参考にしてください。

ただし、現状Firefoxの報告はちょっとおかしいことがあるようです。違反でないものも報告されることがあります。でも、ChromeよりFirefoxのレポートの方が詳しいのでわかりやすいです。

また、CSPには実際にブラウザにポリシーを反映させずに違反を報告させるだけのReport Onlyモードがあります。本番サイトをReport Onlyモードで稼働させれば、CSPを実際に適用する前に検証することができます(後述)。

FuelPHPに導入してみる

サンプルとしてFuelPHPでCSPを導入してみます。

kenjis/cspのインストール

まず、FuelPHP 1.7.xをインストールします。

$ composer create-project fuel/fuel:dev-1.7/master fuel-csp

次にkenjis/cspをインストールします。

$ cd fuel-csp
$ composer require kenjis/csp

アプリケーションブートストラップに以下を追加します。

--- a/fuel/app/bootstrap.php
+++ b/fuel/app/bootstrap.php
@@ -2,6 +2,7 @@
 // Bootstrap the framework DO NOT edit this
 require COREPATH.'bootstrap.php';

+class_alias('Kenjis\Csp\CspStaticProxy', 'Csp');

 Autoloader::add_classes(array(
    // Add classes you want to override here

CSP違反レポートを受信するスクリプトをWeb公開領域にコピーし、

$ cp fuel/vendor/kenjis/csp/csp-report.php public/

パスなどを調整します。きちんとしたい場合は、csp-report.phpに相当するコントローラを作成してください。

--- a/public/csp-report.php
+++ b/public/csp-report.php
@@ -1,6 +1,6 @@
 <?php

-require __DIR__ . '/bootstrap.php';
+require __DIR__ . '/../fuel/vendor/autoload.php';

 use Kenjis\Csp\Logger\File;
 use Kenjis\Csp\CspReport;
@@ -12,7 +12,7 @@ if (! $post) {
     exit;
 }

-$logfile = __DIR__ . '/csp-report.log';
+$logfile = __DIR__ . '/../fuel/app/logs/csp-report.log';
 $logger = new File($logfile);
 $report = new CspReport($logger);
 $report->process($post);

CSPを使ってみる

Baseコントローラを作成し、CSPを使うページではBaseコントローラを継承することにします。全ページでCSPヘッダが同一なら、bootstrap.phpにCsp::sendHeader();を追加する方法も考えられます。

fuel/app/controller/base.php

<?php

class Controller_Base extends Controller
{
    public function before()
    {
        Csp::sendHeader();
    }
}

Controller_Baseを継承してコントローラを作成します。

fuel/app/controller/csp.php

<?php

class Controller_Csp extends Controller_Base
{
    public function action_index()
    {
        return Response::forge(View::forge('csp/index'));
    }
}

これで、このページではCSPヘッダが出力されます。

それでは、ビューを作成します。

fuel/app/view/csp/index.php

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>CSPのサンプル</title>
</head>
<body>

<script type="text/javascript" nonce="<?= Csp::getNonce() ?>">
    alert('これは実行されます。');
</script>

<script type="text/javascript">
    alert('これは実行されません。');
</script>

</body>
</html>

すべてのscriptタグにnonce="<?= Csp::getNonce() ?>"を追加します。

これで完了です。

Report Onlyにしてみる

BaseコントローラにCsp::setReportOnly();を追加します。

fuel/app/controller/base.php

<?php

class Controller_Base extends Controller
{
    public function before()
    {
        Csp::setReportOnly();
        Csp::sendHeader();
    }
}

これで違反の報告のみになります。

ポリシーを追加する

ポリシーを追加してキツくすることもできます。

例えば、デフォルトのポリシーを「外部オリジンからのすべてのリソースを拒否」にするには、以下のようにポリシーを追加設定します。

fuel/app/controller/base.php

<?php

class Controller_Base extends Controller
{
    public function before()
    {
        Csp::addPolicy('default-src', 'self');
//        Csp::setReportOnly();
        Csp::sendHeader();
    }
}

これで、例えば、WelcomeコントローラにCSPを適用します。

--- a/fuel/app/classes/controller/welcome.php
+++ b/fuel/app/classes/controller/welcome.php
@@ -19,7 +19,7 @@
  * @package  app
  * @extends  Controller
  */
-class Controller_Welcome extends Controller
+class Controller_Welcome extends Controller_Base
 {

    /**

この状態でトップページにアクセスすると、インラインのstyleタグも無効になりWelcomeページの頭のFuelPHPのロゴがでなくなります。

FuelPHP Welcomeページのスクリーンショット(インラインstyle禁止)

ちなみに通常のWelcomeページは以下です。

FuelPHP Welcomeページのスクリーンショット(通常)

レポートのログファイルには以下のように記録されます。

fuel/app/logs/csp-report.log

{
    "csp-report": {
        "blocked-uri": "self",
        "document-uri": "http://localhost:8000/",
        "line-number": 7,
        "original-policy": "script-src 'nonce-2LP+8osSXUq97nxHbELPkg=='; report-uri http://localhost:8000/csp-report.php; default-src http://localhost:8000",
        "referrer": "",
        "script-sample": "\n\t\t#logo{\n\t\t\tdisplay: block;\n\t\t\tbackgrou...",
        "source-file": "http://localhost:8000/",
        "violated-directive": ""
    },
    "date": "2014-10-29 23:38:57",
    "headers": {
        "Host": "localhost:8000",
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "ja,en-us;q=0.7,en;q=0.3",
        "Accept-Encoding": "gzip, deflate",
        "DNT": "1",
        "Content-Length": "391",
        "Content-Type": "application/json",
        "Connection": "keep-alive"
    }
}

参考

Date: 2014/10/30

Tags: php, xss, csp, security, fuelphp