本当は危ない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

Tags: codeigniter, codeigniter4, security