CodeIgniter4でCSPのレポートを取得する

設定

diff --git a/app/Config/App.php b/app/Config/App.php
index 1a5e562dd..03e8eb649 100644
--- a/app/Config/App.php
+++ b/app/Config/App.php
@@ -461,5 +461,5 @@ class App extends BaseConfig
      *
      * @var bool
      */
-    public $CSPEnabled = false;
+    public $CSPEnabled = true;
 }
diff --git a/app/Config/ContentSecurityPolicy.php b/app/Config/ContentSecurityPolicy.php
index aa18ba9f1..3d863a634 100644
--- a/app/Config/ContentSecurityPolicy.php
+++ b/app/Config/ContentSecurityPolicy.php
@@ -32,7 +32,7 @@ class ContentSecurityPolicy extends BaseConfig
      *
      * @var string|null
      */
-    public $reportURI;
+    public $reportURI = '/csp-report';

     /**
      * Instructs user agents to rewrite URL schemes, changing
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index ff2ac645c..9d0907edc 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -37,6 +37,8 @@ $routes->set404Override();
 // route since we don't have to scan directories.
 $routes->get('/', 'Home::index');

+$routes->post('csp-report', 'CspReport::index');
+
 /*
  * --------------------------------------------------------------------
  * Additional Routing

コントローラ

<?php

namespace App\Controllers;

use CodeIgniter\I18n\Time;
use stdClass;

class CspReport extends BaseController
{
    private string $logfile = WRITEPATH . 'logs/csp-report.log';

    public function index()
    {
        $log = $this->createLogEntry();

        $this->addRequestHeaders($log);
        $this->addCspReport($log);
        $this->writeToLogfile($log);

        return $this->response->setStatusCode(204);
    }

    private function createLogEntry(): stdClass
    {
        $log = new stdClass();

        $log->date = Time::now()->format('Y-m-d H:i:s');

        return $log;
    }

    private function addRequestHeaders(stdClass $log): void
    {
        foreach ($this->request->headers() as $name => $value) {
            $log->headers[$name] = (string) $value;
        }
    }

    private function addCspReport(stdClass $log): void
    {
        /** @var stdClass|null $report */
        $report = $this->request->getJSON();

        if ($report !== null && json_last_error() === JSON_ERROR_NONE) {
            $log->{'csp-report'} = $report->{'csp-report'};
        }
    }

    private function writeToLogfile(stdClass $log): void
    {
        /** @var string $json */
        $json = json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

        file_put_contents($this->logfile, $json . "\n", LOCK_EX | FILE_APPEND);
    }
}

参考

Tags: codeigniter, codeigniter4, csp, logging

SameSite攻撃者がCodeIgniter4とShieldでのCSRF保護を回避できる脆弱性の解説

CodeIgniter4とCodeIgniter Shieldでの組み合わせで、CSRF保護を回避できる脆弱性に関するセキュリティ勧告が2022/08/08に公表されました。今日は、この脆弱性について解説しておきます。

なお、この攻撃方法はCodeIgniterに限定されるものではありません。

修正済みのバージョン

  • CodeIgniter 4.2.3
  • CodeIgniter Shield 1.0.0-beta.2

前提条件

この脆弱性を攻撃するには、攻撃者が攻撃対象のサイトと同じドメインのサブドメインサイトを支配下に置いている必要があります。

簡単に言えば、サブドメインサイトのページを書き換えられるということです。これはそのサーバーを乗っ取っていたり、そのサブドメインサイトにXSS脆弱性が1つあればOKです。

このような攻撃者を「SameSite攻撃者」と呼んでいます。

攻撃者はそのサブドメインサイトを使い、攻撃対象ドメインのCookieを発行します。

攻撃方法

この脆弱性は、CSRF保護がCookieベースかSessionベースかに関わらず(Config\Security::$csrfProtection'cookie' でも 'session'でも)、 また、CSRFトークンを再生成するか否かに関わらず(Config\Security::$regeneratetrue でも false でも)攻撃可能です。

CookieベースのCSRF保護の場合

CodeIgniter3及びCodeIgniter4は、デフォルトでは、CookieベースのCSRF保護機能を提供します。

CookieにCSRFトークンをセットして、POSTされるCSRFトークンと一致すればOKという実装です。

この場合、攻撃者は支配下にあるサブドメインサイトを使い、被害者のブラウザにCSRFトークンのCookieをセットします。

これで攻撃者はCSRFトークンの値を知っていますので、被害者に同じ値をPOSTさせれば、CSRF保護を突破できます。

そもそもCookieベースのCSRF保護はCookieの改竄ができないことが前提なので、攻撃者がCookieを書き込める場合は安全とは言えません(CodeIgniter3のSecurityクラスのCSRF対策を把握しておく 参照)。

SessionベースのCSRF保護の場合

CodeIgniter4では、設定によりより安全なSessionベースのCSRF保護機能を使えます。

SessionにCSRFトークンをセットして、POSTされるCSRFトークンと一致すればOKという実装です。

この場合、攻撃者は支配下にあるサブドメインサイトを使い、被害者のブラウザにセッションCookieをセットします。 セッション固定化攻撃のテクニックです。

その状態で被害者がサイトにログインすると、セッションIDが更新されるのですが、古いセッションデータが残っていました。

ここで、ログインフォームにCSRF保護があると(グローバルにCSRFフィルタを設定していればそうなります)、古いセッションにもCSRFトークンが保存されています。

フォーム投稿時にCSRFトークンがチェックされ、OKなら、CSRFトークンの値が更新されます(デフォルト設定の場合)。しかし、古いセッションにも、更新された後のCSRFトークンが保存されていました。

攻撃者は、古いセッションCookieでサイトにアクセスすることで、認証前のセッションを乗っ取ることができ、更新されたCSRFトークンの値を知ることができました。

これは、CSRF保護がコントローラフィルタで実装されており、ログイン処理よりも前に実行され、CSRFトークンが更新されるためです。

なお、CSRFトークンを再生成しない設定であれば、CSRFトークンの値は変更されませんので、CSRFトークンが保存されているセッションのセッションCookieを被害者のブラウザにセットすれば攻撃可能です。

対策

対策としては、以下の3つを全て行うことです。

  • SessionベースのCSRF保護を利用する
  • ログイン直後にセッションを再生成し、古いセッションデータを必ず破棄する
  • セッションを再生成した直後にCSRFトークンを必ず再生成する

CookieベースのCSRF保護について

CookieベースのCSRF保護をどうしても利用せざるを得ない場合は、CSRFトークンの値をユーザに紐付けることで対策は不可能ではありません。ユーザを特定できる何らかの値(セッションIDやアクセストークンなど)をキーにして暗号的にCSRFトークンを導出し、他のユーザのために作成されたCSRFトークンがCookieに含まれる場合はエラーにします。

しかし、 ログイン機能などですでにセッションを利用している状況でCookieベースのCSRF保護を使う理由はありません。

なお、Cookieに保存するCSRFトークンの値を暗号化するだけでは対策になりません。攻撃者が攻撃対象サイトにアクセスして暗号化された正規のCSRFトークンの値をCookieから取得すればいいだけです。

参考

Tags: codeigniter, codeigniter4, csrf, security