PHPでCSP(Content Security Policy)を導入してXSS対策を強化してみよう
PHPで簡単にCSPを導入するためのライブラリを作成してみました。
既存サイトへの影響を最小限にして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のロゴがでなくなります。
ちなみに通常の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