CodeIgniter4のCLIジェネレータのテンプレートをカスタマイズする

この記事は CodeIgniter Advent Calendar 2022 - Qiita の10日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

CLIジェネレータとは?

CodeIgniter4 にはコマンドラインからアプリの各種ファイルを生成するCLIジェネレータが付属しています。 spark make:*コマンドです。

ジェネレータを利用することで、コントローラ、モデル、フィルタ、マイグレーションなどのファイルを作成できます。

モデルファイルの生成

例えば、以下のコマンドでモデルファイルを作成できます。

$ php spark make:model product

app/Models/Product.php が作成されました。

<?php

namespace App\Models;

use CodeIgniter\Model;

class Product extends Model
{
    protected $DBGroup          = 'default';
    protected $table            = 'products';
    protected $primaryKey       = 'id';
    protected $useAutoIncrement = true;
    protected $insertID         = 0;
    protected $returnType       = 'array';
    protected $useSoftDeletes   = false;
    protected $protectFields    = true;
    protected $allowedFields    = [];

    // Dates
    protected $useTimestamps = false;
    protected $dateFormat    = 'datetime';
    protected $createdField  = 'created_at';
    protected $updatedField  = 'updated_at';
    protected $deletedField  = 'deleted_at';

    // Validation
    protected $validationRules      = [];
    protected $validationMessages   = [];
    protected $skipValidation       = false;
    protected $cleanValidationRules = true;

    // Callbacks
    protected $allowCallbacks = true;
    protected $beforeInsert   = [];
    protected $afterInsert    = [];
    protected $beforeUpdate   = [];
    protected $afterUpdate    = [];
    protected $beforeFind     = [];
    protected $afterFind      = [];
    protected $beforeDelete   = [];
    protected $afterDelete    = [];
}

しかし、全てのプロパティがオーバーライドされており長いです。

モデルのテンプレートのカスタマイズ

実は、ジェネレータのテンプレートは簡単にカスタマイズできます。

モデルのテンプレートは、 vendor/codeigniter4/codeigniter4/system/Commands/Generators/Views/model.tpl.php にあります。これを app/Commands/Generators/Views/フォルダを作成し、そこにコピーします。

コピーした app/Commands/Generators/Views/model.tpl.php を好きなように変更します。

次に、app/Config/Generators.php にジェネレータの設定クラスがあるので変更します。

--- a/app/Config/Generators.php
+++ b/app/Config/Generators.php
@@ -32,7 +32,7 @@ class Generators extends BaseConfig
         'make:entity'       => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
         'make:filter'       => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
         'make:migration'    => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
-        'make:model'        => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
+        'make:model'        => 'App\Commands\Generators\Views\model.tpl.php',
         'make:seeder'       => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
         'make:validation'   => 'CodeIgniter\Commands\Generators\Views\validation.tpl.php',
         'session:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',

これでOKです。

再度、モデルを生成してみましょう。

$ php spark make:model product --force

今度は以下が作成されました。

<?php

namespace App\Models;

use CodeIgniter\Model;

class Product extends Model
{
    protected $table            = 'products';
    protected $primaryKey       = 'id';
    protected $useAutoIncrement = true;
    protected $returnType       = 'array';
    protected $useSoftDeletes   = false;
    protected $allowedFields    = [];

    // Dates
    protected $useTimestamps = false;
    protected $dateFormat    = 'datetime';
    protected $createdField  = 'created_at';
    protected $updatedField  = 'updated_at';
    protected $deletedField  = 'deleted_at';
}

まとめ

  • CodeIgniter4にはアプリのファイルを作成するCLIジェネレータが付属しています。
  • 生成されるファイルのテンプレートは簡単にカスタマイズできます。

この記事は CodeIgniter Advent Calendar 2022 - Qiita の10日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

関連

参考

Tags: codeigniter, codeigniter4

CodeIgniter4 Bonfire2を試す

この記事は CodeIgniter Advent Calendar 2022 - Qiita の8日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

CodeIgniter4 のアプリケーションスケルトンである Bonfire2 を使ってみます。 Bonfire2 は現在ベータリリースです。

動作確認環境

  • CodeIgniter 4.2.10
  • Bonfire2 dev-develop d1fb7a0
  • PHP 8.1.13
  • MySQL 5.7
  • macOS 10.15.7

Bonfire2 とは?

Bonfire2 は、CodeIgniter4ベースのアプリケーションのための堅牢なアプリケーションスケルトンです。

Bonfireは、クライアントのためによりよいソフトウェアをより速く作ることを支援するために、多くの有用なライブラリを提供し、アプリケーションにとって重要な新しい部分に集中することを可能にします。

現在、以下の機能が含まれています。

  • テーマ/テンプレートシステム:柔軟なAuthテーマとAdminテーマを同梱
  • 再利用可能なHTMLスニペットを作成し、UIの複雑さを軽減するビューコンポーネント(オプションでコードによる制御が可能)
  • 設定ファイルの値をデータベースに保存し、データベース内またはファイル内にある値にアクセスできるSettingsライブラリ
  • リソースフィルタシステムは、ユーザー、投稿などのリストをフィルタリングするために、簡単な実装と快適で一貫したUIを提供
  • 強力でカスタマイズ可能なユーザー認証/認可システムShield
  • モジュールに簡単に統合できるグローバル検索機能
  • モジュールに簡単に統合できる、論理削除モデルの復元/消去を処理するためのごみ箱
  • GDPRルールに対応したCookieの同意管理方法
  • サイトオフラインステータス
  • オンラインログビューワ/マネージャー

インストール

CodeIgniter4のインストール

ComposerでCodeIgniterプロジェクトを作成します。

$ composer create-project codeigniter4/appstarter ci4-bonfire2-test

Bonfire2のインストール

Composerの minimum-stability の設定を変更します。

--- a/composer.json
+++ b/composer.json
@@ -33,5 +33,7 @@
         "forum": "http://forum.codeigniter.com/",
         "source": "https://github.com/codeigniter4/CodeIgniter4",
         "slack": "https://codeigniterchat.slack.com"
-    }
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true
 }

ComposerでBonfire2の最新開発版をインストールします。

$ cd ci4-bonfire2-test/
$ composer require lonnieezell/bonfire:dev-develop

データベースの作成

専用のデータベース bonfire2 を作成しておきます。

CREATE DATABASE `bonfire2` DEFAULT CHARACTER SET utf8mb4;
CREATE USER dbuser@localhost IDENTIFIED WITH mysql_native_password BY 'dbpasswd';
GRANT ALL PRIVILEGES ON bonfire2.* TO dbuser@localhost;

設定

CodeIgniter4の設定

以下のように設定を日本仕様などに変更しておきます。

--- a/app/Config/App.php
+++ b/app/Config/App.php
@@ -37,7 +37,7 @@ class App extends BaseConfig
      *
      * @var string
      */
-    public $indexPage = 'index.php';
+    public $indexPage = '';

     /**
      * --------------------------------------------------------------------------
@@ -70,7 +70,7 @@ class App extends BaseConfig
      *
      * @var string
      */
-    public $defaultLocale = 'en';
+    public $defaultLocale = 'ja';

     /**
      * --------------------------------------------------------------------------
@@ -97,7 +97,7 @@ class App extends BaseConfig
      *
      * @var string[]
      */
-    public $supportedLocales = ['en'];
+    public $supportedLocales = ['en', 'ja'];

     /**
      * --------------------------------------------------------------------------
@@ -109,7 +109,7 @@ class App extends BaseConfig
      *
      * @var string
      */
-    public $appTimezone = 'America/Chicago';
+    public $appTimezone = 'Asia/Tokyo';

     /**
      * --------------------------------------------------------------------------

Bonfire2はCodeIgniter Shieldを使っているので、CSRFの設定を変更します。

--- a/app/Config/Security.php
+++ b/app/Config/Security.php
@@ -15,7 +15,7 @@ class Security extends BaseConfig
      *
      * @var string 'cookie' or 'session'
      */
-    public $csrfProtection = 'cookie';
+    public $csrfProtection = 'session';

     /**
      * --------------------------------------------------------------------------

Bonfire2のセットアップ

セットアップコマンドを実行します。

$ php spark bf:install
CodeIgniter v4.2.10 Command Line Tool - Server Time: 2022-12-06 16:25:48 UTC+09:00

Creating .env file...Done
Setting initial environment

What URL are you running Bonfire under locally? : http://localhost:8080/

Generating encryption key
Encryption key saved to .env file

Database host:  [localhost]: 
Database name:  [bonfire]: bonfire2
Database username:  [root]: dbuser
Database password:  [root]: dbpasswd
Database driver: [MySQLi, Postgre, SQLite3, SQLSRV]: 
Table prefix: : 

Publishing config files
  Created: APPPATH/Config/Assets.php
  Created: APPPATH/Config/Auth.php
  Created: APPPATH/Config/AuthGroups.php
  Created: APPPATH/Config/Bonfire.php
  Created: APPPATH/Config/Site.php
  Created: APPPATH/Config/Themes.php
  Created: APPPATH/Config/Consent.php
  Created: APPPATH/Config/Dashboard.php
  Created: APPPATH/Config/Recycler.php
  Created: APPPATH/Config/Users.php

If you need to create your database, you may run:
    php spark db:create <database name>

To migrate and create the initial user, please run: 
    php spark bf:install --continue

データベース接続情報を聞かれますので、入力します。

完了すると、.envファイルが作成され、設定ファイルとテーマファイルがインストールされました。

セットアップを続けます。

$ php spark bf:install --continue
CodeIgniter v4.2.10 Command Line Tool - Server Time: 2022-12-06 16:28:10 UTC+09:00

Running all new migrations...
    Running: (CodeIgniter\Shield) 2020-12-28-223112_CodeIgniter\Shield\Database\Migrations\CreateAuthTables
    Running: (CodeIgniter\Settings) 2021-07-04-041948_CodeIgniter\Settings\Database\Migrations\CreateSettingsTable
    Running: (Bonfire\{Users}) 2021-09-04-044800_App\Database\Migrations\AdditionalUserFields
    Running: (Bonfire\{Users}) 2021-10-05-040656_App\Database\Migrations\CreateMetaTable
    Running: (CodeIgniter\Settings) 2021-11-14-143905_CodeIgniter\Settings\Database\Migrations\AddContextColumn
Migrations complete.

Create initial user
Email? : admin@example.jp
First name? : Admin
Last name? : User
Username? : admin
Password? : passw0rd!
Done. You can now login as a superadmin.

マイグレーションが実行され、初期ユーザーの情報を入力すると、superadmin のユーザーが作成されました。

以下のテーブルが作成されています。

$ php spark db:table --metadata

CodeIgniter v4.2.10 Command Line Tool - Server Time: 2022-12-06 16:30:03 UTC+09:00

Here is the list of your database tables:
  [0]  auth_groups_users
  [1]  auth_identities
  [2]  auth_logins
  [3]  auth_permissions_users
  [4]  auth_remember_tokens
  [5]  auth_token_logins
  [6]  meta_info
  [7]  migrations
  [8]  settings
  [9]  users

migrations は CodeIgniter4 のデータベースマイグレーション用、settings は codeigniter4/settings 用、auth_*users は codeigniter4/shield 用のテーブルです。

Webサーバーの起動

開発用にWebサーバーを起動します。

$ php spark serve

管理者ログイン

ログインページ

http://localhost:8080/admin にブラウザでアクセスします。

http://localhost:8080/login にリダイレクトされました。

先ほど作成したユーザーでログインします。

    メール:admin@example.jp
パスワード:passw0rd!

ログインが成功すると、ダッシュボードにリダイレクトされます。

ユーザーの一覧。

Cookie同意モジュールの設定。

メールの設定。

一般設定。

ユーザーグループ設定。

ユーザーに関する設定。

ウィジットの設定。

ログ閲覧ツール。

システム情報。

マイアカウント。

右上のアイコンから、ログアウトします。

ユーザー登録

http://localhost:8080/register にブラウザでアクセスします。

登録ページが表示されました。

ユーザーを登録します。

完了すると、トップページにリダイレクトされました。

デバッグバーを表示すると、ユーザーとしてログインしていることがわかります。

まとめ

  • Bonfire2はCodeIgniter4用のアプリケーションスケルトンです。
  • Bonfire2を使うとWebアプリケーションを素早く開発することができます。
  • Bonfire2はCodeIgniter Shieldを利用しており、ユーザー管理機能を含む管理ページを提供します。

この記事は CodeIgniter Advent Calendar 2022 - Qiita の8日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

関連

参考

Tags: codeigniter, codeigniter4

CodeIgniter 4.1.5 までのオブジェクトインジェクション脆弱性

この記事は CodeIgniter Advent Calendar 2022 - Qiita の7日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

CodeIgniter 4.1.6 で修正された CVE-2022-21647 Deserialization of Untrusted Data in Codeigniter4 の解説です。

オブジェクトインジェクションが何かについては、 PHPにおけるオブジェクトインジェクション脆弱性について を参照してください。

脆弱性のあったコード

以下が実際のコードです。

https://github.com/codeigniter4/CodeIgniter4/blob/8c7f70188e2bb7f19e6c7fcbbadb4dcb94c1cbc7/system/Common.php#L788-L823

    /**
     * Provides access to "old input" that was set in the session
     * during a redirect()->withInput().
     *
     * @param null        $default
     * @param bool|string $escape
     *
     * @return mixed|null
     */
    function old(string $key, $default = null, $escape = 'html')
    {
        // Ensure the session is loaded
        if (session_status() === PHP_SESSION_NONE && ENVIRONMENT !== 'testing') {
            // @codeCoverageIgnoreStart
            session();
            // @codeCoverageIgnoreEnd
        }

        $request = Services::request();

        $value = $request->getOldInput($key);

        // Return the default value if nothing
        // found in the old input.
        if ($value === null) {
            return $default;
        }

        // If the result was serialized array or string, then unserialize it for use...
        if (is_string($value) && (strpos($value, 'a:') === 0 || strpos($value, 's:') === 0)) {
            $value = unserialize($value); // ここが問題
        }

        return $escape === false ? $value : esc($value, $escape);
    }
}

最後の if文の中で $valueunserialize() しています。

では、$value とは何でしょうか?

        $value = $request->getOldInput($key);

上記のコードが $value に値を代入しています。

$requestIncomingRequest のオブジェクトです。 IncomingRequest::getOldInput() メソッドは以下のようになっていました。

    /**
     * Attempts to get old Input data that has been flashed to the session
     * with redirect_with_input(). It first checks for the data in the old
     * POST data, then the old GET data and finally check for dot arrays
     *
     * @return mixed
     */
    public function getOldInput(string $key)
    {
        // If the session hasn't been started, or no
        // data was previously saved, we're done.
        if (empty($_SESSION['_ci_old_input'])) {
            return null;
        }

        // Check for the value in the POST array first.
        if (isset($_SESSION['_ci_old_input']['post'][$key])) {
            return $_SESSION['_ci_old_input']['post'][$key];
        }

        // Next check in the GET array.
        if (isset($_SESSION['_ci_old_input']['get'][$key])) {
            return $_SESSION['_ci_old_input']['get'][$key];
        }

        helper('array');

        // Check for an array value in POST.
        if (isset($_SESSION['_ci_old_input']['post'])) {
            $value = dot_array_search($key, $_SESSION['_ci_old_input']['post']);
            if ($value !== null) {
                return $value;
            }
        }

        // Check for an array value in GET.
        if (isset($_SESSION['_ci_old_input']['get'])) {
            $value = dot_array_search($key, $_SESSION['_ci_old_input']['get']);
            if ($value !== null) {
                return $value;
            }
        }

        // requested session key not found
        return null;
    }

セッションに保存されたデータを返すことがわかります。

では、$_SESSION['_ci_old_input'] はどこでセットされるのでしょうか? _ci_old_input でソースコードを検索すると、RedirectResponse::withInput() メソッドでした。

    /**
     * Specifies that the current $_GET and $_POST arrays should be
     * packaged up with the response.
     *
     * It will then be available via the 'old()' helper function.
     *
     * @return $this
     */
    public function withInput()
    {
        $session = Services::session();

        $session->setFlashdata('_ci_old_input', [
            'get'  => $_GET ?? [],
            'post' => $_POST ?? [],
        ]);

        // If the validation has any errors, transmit those back
        // so they can be displayed when the validation is handled
        // within a method different than displaying the form.
        $validation = Services::validation();

        if ($validation->getErrors()) {
            $session->setFlashdata('_ci_validation_errors', serialize($validation->getErrors()));
        }

        return $this;
    }

このメソッドはリダイレクトする際に、入力データをフラッシュセッションに保存するメソッドのようです。 スーパーグローバルの $_GET$_POST をそのままセッションに保存しています。

ということで、$value には直前ページでの入力データが代入されることがわかりました。

そして、$value の値をチェックしてから、unserialize()関数に渡しています。

        // If the result was serialized array or string, then unserialize it for use...
        if (is_string($value) && (strpos($value, 'a:') === 0 || strpos($value, 's:') === 0)) {
            $value = unserialize($value);
        }

$value の値はGETまたはPOSTデータなので、文字列であることは確定しています。

strpos($value, 'a:') === 0 || strpos($value, 's:') === 0 でシリアライズされた値が配列か文字列であるかをチェックしたつもりかも知れませんが、このチェックは不十分です。配列の値にオブジェクトを含めればいいだけだからです。

結果として、オブジェクトインジェクションが可能であり、脆弱性のあるコードです。

脆弱性の修正

オブジェクトインジェクションの対策は、unserialize()関数にユーザー入力を渡さないことです。

実際の修正は、問題のあった if文が丸ごと削除されています。

https://github.com/codeigniter4/framework/commit/5b34d725f94fb0675d761bc55cb886480c85a5a4#diff-99c83f6a1ccaee1c1ebabad8faa8dac62c69ecee1bab9ec323ea127c6a2263d0L816-L820

なぜこの脆弱性が発生したのか?

そもそもなぜ入力データを unserialize() していたのかは、よくわかりません。

通常、ユーザーが HTMLフォームでシリアライズされた文字列を送信してくることは考えられませんから、 それをわざわざデシリアライズする必要もありません。

何らかの機能を意図したものだったのでしょうが、ドキュメントにも記述はありません。

根本的な問題は、ユーザー入力を正しく検証せずに使っていたことです。

しかし、シリアライズされたデータの検証は難しいので、ユーザー入力を unserialize() に一切渡さないことをお薦めします。 その方が簡単で確実です。

まとめ

  • ユーザー入力を unserialize()関数に渡してはいけません。脆弱性を作り込むことになります。
  • unserialize() を使う前に、JSONが使えないか検討しましょう。
  • 外部に保存されているシリアル化されたデータをデシリアライズする必要がある場合は、hash_hmac() を使ったデータの検証を検討しましょう。

この記事は CodeIgniter Advent Calendar 2022 - Qiita の7日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。

参考

Tags: codeigniter, codeigniter4, security, php