⛱️

Laravel (環境によっては)認可系などの比較にご注意を

2021/06/01に公開

はじめに

よく、自分の所有でないブログを編集しようとしたら、エラーにする、というような処理を入れたりします。いわゆる認可ですね。
そして、以下のようなコードなどを書いたりします。

    if ($post->user_id !== $request->user()->id) {
        // 本人でないのでエラー
    }

通常というか、多くの場合は問題は無いのですが、一部サーバ環境ではこれだとまずかったりします。
私の知る限りでは、Xserverで、PHP7.4以外の時は、まずいです。
実際、どれだけのサーバでこういう状況になるかは分かりませんが、一部環境ではあり得る、となります。

何が問題?

上記では、型もチェックする比較(!==)を使っていますが、環境によっては、ここで問題が発生します。問題が発生する際は $post->user_id が string 型で、$request->user()->id は、integer 型となります。上記の場合は、そもそも型が異なるので、本人でさえも、本人でないと判定されます。

その逆(本人以外が本人と判定される)でないだけいいですが、場合によっては、そういうマズイ判定になるケースも最後に見てみます。

通常(多くの場合)、integer型で定義していれば、$post->user_id も integer 型で取得できます。この挙動の違いは、PDOにあるようで、mysqlndが入っていない場合、又は入っていても PDO::ATTR_EMULATE_PREPARES が true の時は、string 型となってしまうようです。

試しに、config/database.php で以下のようにやって自分のサーバで試してみたら、確かに string 型で取得されました。

        'mysql' => [
            // 

            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
                PDO::ATTR_EMULATE_PREPARES => true, // 追加
            ]) : [],
        ],

参考:mysqlndモジュールが効かない時のTIPS

原因についてはさておき、Laravel的にどう対応すれば良いか、以下対処法です。

対処法

(1) 型をチェックしないで比較

!== ではなく、!= にしてしまう方法です。一番手っ取り早いかも知れませんが…。(又は明示的に型変換して比較するとか)

    if ($post->user_id != $request->user()->id) {
        // 本人でないのでエラー
    }

(2) 比較の対象を同じにする

以下のように同じデータ(User側のID)同士を比較してやれば !== で問題ありません。

    if ($post->user->id !== $request->user()->id) {
        // 本人でないのでエラー
    }

(3) 上記(2)をLaravelユーザーっぽくやる

    if ($request->user()->isNot($post->user)) {
        // 本人でないのでエラー
    }

これが一番お勧めですね。しかもこちらの場合、(2)の場合と異なり、仮に $post->user が null でも「Attempt to read property "id" on null」のようなエラーは出ません。
isNot() の反対の is() もあります。

(4) cast の設定をする

以下のように user_id をキャストする設定をするという手もあります。

class Post extends Model
{
    protected $casts = [
        'user_id' => 'integer',
    ];
}

ちなみに、何故 user_id は、string 型で取得されるのに、id の方は integer で取得できるのだという話もありますが、Laravelではデフォルトで、primaryKeyについては、integer 型にキャストしているから、となります。

参考:GitHub [5.2] Cast incrementing IDs to Integer by default

場合によっては、まずい問題になる事も

if ($user->type === 1) {
	// 一般の方は閲覧できないのでエラー
}

type
1:一般ユーザー
2:管理者等
として、typeでinteger型を期待しているのに、環境によってstringだった場合、まずいですね。

雑感

現状、どれ位こういうサーバがあるのでしょうか。
今回は認可に特化して話を書きましたが、認可だけでの問題ではないと思います。

間違いやより良い書き方などありましたらコメント下さい。

参考:GitHub Laravel Issues
参考:stackoverflow
参考:stackoverflow

Discussion