本当は危ない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日目です。まだ、空きがありますので、興味のある方は気軽に参加してください。
関連
参考
Date: 2022/12/24



![徹底攻略PHP5技術者認定[上級]試験問題集  [PJ0-200]対応 徹底攻略PHP5技術者認定[上級]試験問題集  [PJ0-200]対応](http://tatsu-zine.com/images/books/164/cover_s.jpg)

