LoginSignup
43
41

More than 1 year has passed since last update.

【Laravel】SQLインジェクション対策(LIKE句)

Last updated at Posted at 2021-10-20

問題

派手なSQLインジェクションは一般的なWebフレームワークを使用すれば基本的に発生しません。
しかし、LIKE検索を行う場合はDoS攻撃が成立してしまうことがあります。
LIKE "%a%b%c%d%e%e%f%g%@%.%"
上記のようなクエリはSQLエンジンに大きな負荷をかけます。
LIKE句のメタ文字はエスケープする必要がありますが、

$query->where('hoge', 'LIKE', '%' . $value . '%');

と直に書いてしまうケースは多いと思います。

対策

LaravelでのこのLIKE句のインジェクション対策はおそらく3通りほどあると思うので、
それぞれのソリューションをご紹介していこうと思います。

1. macroを用意する

まずBlueprintのmacroを定義するために、
サービスプロバイダを新しく作ります。(AppServiceProvider.phpに書き込む方法もある。

php artisan make:provider BlueprintServiceProvider

すると、以下のようなファイルが生成されます。

app/Providers/BlueprintServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class BlueprintServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

このbootメソッドにmacroを定義していきます。

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        Builder::macro('whereLike', function (string $attribute, string $keyword, int $position = 0) {

            $keyword = addcslashes($keyword, '\_%');

            $condition = [
                    1  => "{$keyword}%",
                    -1 => "%{$keyword}",
                ][$position] ?? "%{$keyword}%";

            return $this->where($attribute, 'LIKE', $condition);
        });

        Builder::macro('orWhereLike', function (string $attribute, string $keyword, int $position = 0) {

           $keyword = addcslashes($keyword, '\_%');

            $condition = [
                    1  => "{$keyword}%",
                    -1 => "%{$keyword}",
                ][$position] ?? "%{$keyword}%";

            return $this->orWhere($attribute, 'LIKE', $condition);
        });
    }

2.クエリスコープを使う

Modelで以下のように定義します。

app/Models/Model.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model as EloquentModel;

/**
 * This class contains shared setup, properties and methods
 * of all application models
 *
 */
class Model extends EloquentModel
{

    public function scopeWhereLike($query, string $attribute, string $keyword, int $position = 0)
    {
        $keyword = addcslashes($keyword, '\_%');

        $condition = [
                1  => "{$keyword}%",
                -1 => "%{$keyword}",
            ][$position] ?? "%{$keyword}%";

        return $query->where($attribute, 'LIKE', $condition);
    }

    public function scopeOrWhereLike($query, string $attribute, string $keyword, int $position = 0)
    {
        $keyword = addcslashes($keyword, '\_%');

        $condition = [
                1  => "{$keyword}%",
                -1 => "%{$keyword}",
            ][$position] ?? "%{$keyword}%";

        return $query->orWhere($attribute, 'LIKE', $condition);
    }

クエリスコープで定義した場合はIDEの支援は効きませんが、ある程度整理整頓ができるかもしれません。

3.Traitで使い回せるパーツとして用意する

macro・クエリスコープ定義ではIDE支援が効かない問題があります。
また、チームの規模によってはフレームワークの理解レベルにバラつきが出てくることもあります。
なので言語レベルで理解のしやすいTraitで機能を用意してあげるという解も出てきます。
TraitならIDEの支援も効くので、新メンバーなどがコードを見たときにコードジャンプ等でたどり着くコストが低くなるかもしれません。

だがしかし、Traitでいい感じに実装する方法がわからなかったので諦めた。

イメージとしては以下の感じで実装できたらよかったのですが、Eloquent\Builder を持てない為
$result = Model::whereLike('hoge', $value)->get() みたいな書き方は出来ても
$result = Model::where('hoge', $value)->orWhereLike('hoge', $value)->get();
みたいなEloquent\Builderの関数から呼び出そうとすると当然コケてしまいます。

なんかいい感じにTraitで実装する方法があったら教えて下さい。

app/Libs/EloquentQueryBuilder.php
<?php

declare(strict_types=1);

namespace App\Libs;

trait EloquentQueryBuilder
{
    protected function whereLike(string $attribute, string $keyword, int $position = 0)
    {
        $keyword = addcslashes($keyword, '\_%');

        $condition = [
              1  => "{$keyword}%",
              -1 => "%{$keyword}",
        ][$position] ?? "%{$keyword}%";

        return $this->orWhere($attribute, 'LIKE', $condition);
    }

    protected function orWhereLike(string $attribute, string $keyword, int $position = 0)
    {
        $keyword = addcslashes($keyword, '\_%');

        $condition = [
            1  => "{$keyword}%",
            -1 => "%{$keyword}",
        ][$position] ?? "%{$keyword}%";

        return $this->orWhere($attribute, 'LIKE', $condition);
    }
}

まとめ

macroかクエリスコープを実装することでEloquentのwhere句を書くのと同様の記法で、

  • whereLike
  • orWhereLike

が使えるようになります。

使い方

whereLike

$result = Model::whereLike('hoge', $keyword)->get();
// or
$query = Model::query();
$result = $query::whereLike('hoge', $keyword)->get();

orWhereLike

$result = Model::where('hoge', $value)->orWhereLike('hoge', $keyword)->get();
// or
$query = Model::query();
$result = $query::where('hoge', $value)->orWhereLike('hoge', $keyword)->get();

あとは、where句に生のLIKE句が混入しないようにコードの品質をキープすれば解決できます。

前述の通りmacroやクエリスコープではIDE支援が効かないので、Laravelに補完・型情報を付与してくれるライブラリLaravel IDE Helper Generatorなどを使用すると便利だと思います。

43
41
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
41