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

# **問題**

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

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

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

# **対策**

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

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

```shell
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を定義していきます。

```php
    /**
     * 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
```php
$result = Model::whereLike('hoge', $keyword)->get();
// or
$query = Model::query();
$result = $query::whereLike('hoge', $keyword)->get();
```

### orWhereLike

```php
$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](https://github.com/barryvdh/laravel-ide-helper)などを使用すると便利だと思います。