CodeIgniter4のCodeIgniter\ModelとQuery Builderの関係

この記事は CodeIgniter Advent Calendar 2022 - Qiita の25日目です。

CodeIgniter\Model とは?

CodeIgniter4で追加された機能です。

これは、データベース内の「1つのテーブル」をより便利に扱うために、よく使う便利な機能や追加機能を提供するものです。

以下のような機能があります。

テーブルに主キーがあることが前提であり、複合主キーには現在(v4.3でも)対応していません。

Query Builderとの関係

CodeIgniter\Model は内部的にテーブル名とそのテーブル用の Query Builder を持ちます。

そのテーブル用のQuery Builder

CodeIgniter\Model を継承したモデルクラス内で、以下でそのテーブル用のQuery Builderを取得できます。

$builder = $this->builder();

CodeIgniter\Model は基本的にこのテーブル用のQuery Builderを共有して処理を実行します。

外部からのQuery Builderメソッドの呼び出し

CodeIgniter\Model では、外部からでもQuery Builderメソッドを直接呼び出すことができます。

具体的な実装は以下です。

    /**
     * Provides direct access to method in the builder (if available)
     * and the database connection.
     *
     * @return mixed
     */
    public function __call(string $name, array $params)
    {
        $builder = $this->builder();
        $result  = null;

        if (method_exists($this->db, $name)) {
            $result = $this->db->{$name}(...$params);
        } elseif (method_exists($builder, $name)) {
            $this->checkBuilderMethod($name);

            $result = $builder->{$name}(...$params);
        } else {
            throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
        }

        if ($result instanceof BaseBuilder) {
            return $this;
        }

        return $result;
    }

CodeIgniter\Model に定義されていないメソッドの場合、

  1. データベース接続オブジェクトに存在するメソッドの場合、それを呼び出す
  2. Query Builderに存在するメソッドの場合、それを呼び出す

そして、結果を返しますが、Query Builderが返ってきた場合は、CodeIgniter\Model 自身を返します。

この実装により、以下のようなコードが実行できるわけです。

$users = $userModel->where('status', 'active')
    ->orderBy('last_login', 'asc')
    ->findAll();

上記で、where()orderBy() はQuery Builderのメソッドで、findAll()CodeIgniter\Model のメソッドです。

しかし、Query BuilderがQuery Builder自身ではなく、何らかの結果を返した場合は、その結果が返されます。

その場合、CodeIgniter\Model のメソッドによって返されるものとは異なり、期待されたものでないかもしれません。Query Builderが結果を返しているので、CodeIgniter\Model のイベントはトリガーされませんし、また、論理削除も考慮されません。

このような期待しない動作を防ぐためには、メソッドチェーンの最後には、Query Builderのメソッドではなく、CodeIgniter\Model のメソッドを指定する必要があります。

結局、どのメソッドが CodeIgniter\Model のメソッドでどのメソッドがQuery Builderのメソッドか区別がつかないと CodeIgniter\Model をうまく使えません。

これが、CodeIgniter\Model がわかりにくいとされる理由です。

この記事は CodeIgniter Advent Calendar 2022 - Qiita の25日目です。

関連

参考

Tags: codeigniter, codeigniter4

本当は危ないCodeIgniter4のModel::update()

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

CodeIgniter Model

CodeIgniter4には CodeIgniter Model が追加されました。

これは、データベース内の 1つのテーブルをより便利に扱うために、よく使う便利な機能や追加機能を提供するものです。

Model::update() の仕様

Model::update($id, $data) はデータベースのレコードを更新するためのメソッドです。

このメソッドには、多くの開発者が予期しない仕様があり、想定していない値を渡された場合、データベース内のデータを破壊される可能性があります。

それは、$id に null などを渡すと(他にWHERE句を指定するメソッドを呼んでいない場合に)、全レコードが更新されるというものです。

つまり、以下はWHERE句のないUPDATE文を実行します。

$this->model->update(null, $data);

なお、CodeIgniter 4.3.0 からは、WHERE句のないUPDATEの場合はエラーになるように変更される予定です。

脆弱性を作らないために

基本的なセキュリティ対策ですが、 IDに想定していない値を渡さないように、確実にバリデーションすることです。

脆弱性のあるコードの例

せっかくなので、脆弱性のサンプルを作成しました。 以下のようなコードは書いてはいけません。

実際に動作するコードをGitHubに置いてあります。

興味のある方は、攻撃方法を考えてみてください。

脆弱性のあるコード例(1)

POSTデータからIDとデータを受け取るパターンですが、IDのバリデーションがされていません。

<?php

namespace App\Controllers;

use App\Models\NewsModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;

class News extends BaseController
{
    private NewsModel $model;

    /**
     * Validation Rules
     */
    private array $rules = [
        'title' => 'required|min_length[3]|max_length[255]',
        'body'  => 'required|min_length[10]|max_length[5000]',
    ];

    public function __construct()
    {
        $this->model = model(NewsModel::class);
    }

    // ...

    /**
     * @return ResponseInterface|string
     */
    public function update()
    {
        $id = $this->request->getPost('id');

        if ($this->request->getMethod() === 'post' && $this->validate($this->rules)) {
            $title = $this->request->getPost('title');
            $slug  = url_title($title, '-', true);

            $data = [
                'title' => $title,
                'slug'  => $slug,
                'body'  => $this->request->getPost('body'),
            ];
            $this->model->update($id, $data);

            return $this->response->redirect(site_url('news/' . $slug));
        }

        return $this->edit($id);
    }
}

脆弱性のあるコード例(2)

自動ルーティング(改善)を使ったケースで、IDは引数で受けています。 これも、IDのバリデーションがされていません。

<?php

namespace App\Controllers;

use App\Models\NewsModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;

class News2 extends BaseController
{
    private NewsModel $model;

    /**
     * Validation Rules
     */
    private array $rules = [
        'title' => 'required|min_length[3]|max_length[255]',
        'body'  => 'required|min_length[10]|max_length[5000]',
    ];

    public function __construct()
    {
        $this->model = model(NewsModel::class);
    }

    // ...

    /**
     * @param false|int $id
     *
     * @return ResponseInterface|string
     */
    public function postUpdate($id = false)
    {
        if ($this->validate($this->rules)) {
            $title = $this->request->getVar('title');
            $slug  = url_title($title, '-', true);

            $data = [
                'title' => $title,
                'slug'  => $slug,
                'body'  => $this->request->getVar('body'),
            ];
            $this->model->update($id, $data);

            return $this->response->redirect(site_url('news2/view/' . $slug));
        }

        return $this->getEdit($id);
    }
}

脆弱性のあるコード例(3)

POSTデータからIDとデータを受け取るパターンで、IDのバリデーションもしているつもりですが...

<?php

namespace App\Controllers;

use App\Models\NewsModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;

class News3 extends BaseController
{
    private NewsModel $model;

    /**
     * Validation Rules
     */
    private array $rules = [
        'title' => 'required|min_length[3]|max_length[255]',
        'body'  => 'required|min_length[10]|max_length[5000]',
    ];

    public function __construct()
    {
        $this->model = model(NewsModel::class);
    }

    // ...

    /**
     * @return ResponseInterface|string
     */
    public function update()
    {
        $id = $this->request->getPost('id');

        // Adds validation rule for id.
        $rules = array_merge($this->rules, ['id' => 'required|is_natural_no_zero']);

        if ($this->validate($rules)) {
            $title = $this->request->getVar('title');
            $slug  = url_title($title, '-', true);

            $data = [
                'title' => $title,
                'slug'  => $slug,
                'body'  => $this->request->getVar('body'),
            ];
            $this->model->update($id, $data);

            return $this->response->redirect(site_url('news3/' . $slug));
        }

        return $this->edit($id);
    }
}

まとめ

  • Model::update() は想定しないIDを受け取ると、データを破壊する可能性があります。
  • 全ての入力値を確実にバリデーションしてから処理しましょう。

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

関連

参考

Tags: codeigniter, codeigniter4, security

CodeIgniter 4.2.11(セキュリティ修正)がリリースされました

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

CodeIgniter v4.2.11

以下の 2件の脆弱性を修正した CodeIgniter 4.2.11 がリリースされました。 既存ユーザーの方は直ちにアップグレードすることを推奨します。

なお、破壊的な変更がありますので、アップグレードガイド に従ってください。

修正された脆弱性:

Attackers may spoof IP address when using proxy

サーバーがリバースプロキシーの後ろに配置された構成の場合に、 この脆弱性を悪用すると、攻撃者はIPアドレスを詐称できる可能性があります。

Potential Session Handlers Vulnerability

アプリケーションが、

  1. 複数のセッションクッキー(例:ユーザーページ用と管理者ページ用)を使用し、
  2. セッションハンドラに DatabaseHandlerMemcachedHandler、または RedisHandler を使用している場合、

攻撃者が、あるセッションクッキー(例:ユーザーページ用)を取得すると、別のセッションクッキー(例:管理者ページ用)を必要とするページにアクセスできる可能性があります。

たぶん、このようにセッションクッキーを 2つ以上使っているユーザーはほとんどいないと思いますので、この脆弱性の影響を受けるユーザーは少ないと思います。

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

関連

参考

Tags: codeigniter, codeigniter4, release, security