問題
派手な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
すると、以下のようなファイルが生成されます。
<?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で以下のように定義します。
<?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で実装する方法があったら教えて下さい。
<?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などを使用すると便利だと思います。