はじめに
近年はセキュリティ意識の高まりから、多くの開発者が「生 SQL」から脱却し、Doctrine / Hibernate / SQLAlchemy / Eloquent などの ORM(Object-Relational Mapping) を利用するようになりました。
ORM は確かに SQL インジェクションのリスクを減らしてくれますが──**「ORM を使っている=インジェクション安全」ではありません。**
本記事では、TryHackMe「ORM Injection」ルームの内容をベースに、
- ORM の基本とメリット
- SQL Injection と ORM Injection の違い
- Laravel(Eloquent)を使った弱い実装・脆弱な実装の例
- 各種 ORM に共通する防御ベストプラクティス
をまとめて解説します。
1. ORM とは何か
1.1 定義
ORM(Object-Relational Mapping) は、オブジェクト指向言語とリレーショナルDB(RDB)の間の「翻訳」をしてくれる技術です。
- アプリ側:クラス・オブジェクトの世界
- DB 側:テーブル・行の世界
本来この二つはかなり世界観が違いますが、ORM は
「クラス ⇔ テーブル」「オブジェクト ⇔ レコード」
を自動的にマッピングし、コード側ではオブジェクトを触るだけで DB を操作できるようにしてくれます。
1.2 ORM を使う目的
主なメリットは以下の通り。
-
ボイラープレート削減
CRUD のための同じような SQL を毎回書かなくて済む -
生産性向上
ビジネスロジックに集中できる -
一貫性の担保
フレームワークが DB 操作のパターンを統一してくれる -
保守性向上
スキーマ変更がクラス定義から追いやすい
1.3 代表的な ORM フレームワーク
言語ごとに有名どころを挙げると:
- PHP:Doctrine, Laravel Eloquent
- Java:Hibernate
- Python:SQLAlchemy
- C#/.NET:Entity Framework
- Ruby:ActiveRecord(Rails 標準)
どれも「オブジェクト⇔テーブル」のマッピングを提供しつつ、クエリ API(HQL / DQL / Query Builder など)を持っています。
2. ORM の基本動作と CRUD
2.1. Laravel Eloquent の例
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'users';
protected $fillable = [
'name', 'email', 'password',
];
}
-
Userクラス ⇔usersテーブル - プロパティ ⇔ カラム
- インスタンス ⇔ 1 レコード
2.2. 典型的な CRUD 操作
Create(作成)
use App\Models\User;
$user = new User();
$user->name = 'Admin';
$user->email = 'admin@example.com';
$user->password = bcrypt('password');
$user->save();
裏では INSERT INTO users ... が実行されます。
Read(読み取り)
use App\Models\User;
// 主キーで取得
$user = User::find(1);
// 全件取得
$allUsers = User::all();
// 条件付き取得
$admins = User::where('email', 'admin@example.com')->get();
ORM の API を使っていますが、内部では
SELECT * FROM users WHERE id = 1SELECT * FROM usersSELECT * FROM users WHERE email = 'admin@example.com'
といった SQL が生成されて実行されています。
Update・Delete も同じノリで、
- レコードを取得
- プロパティを書き換える or delete()
- ORM が
UPDATE/DELETEを発行
という流れです。
3. SQL Injection と ORM Injection の違い
3.1. SQL Injection
ターゲットは 生 SQL。
// 危険な例
$username = $_GET['username'];
$sql = "SELECT * FROM users WHERE username = '$username'";
admin' OR '1'='1 のような入力を入れると、
SELECT * FROM users WHERE username = 'admin' OR '1'='1';
となり、条件が常に true になってしまう典型パターンです。
3.2 ORM Injection
ターゲットは ORM のクエリ構築部分。
// リポジトリに渡す値をそのまま使っている例
$userRepository->findBy(['username' => "admin' OR '1'='1"]);
ORM が内部で組み立てる SQL に、ユーザー入力が「安全でない形」で差し込まれてしまうと、結果として SQL Injection と同様の現象が起こります。
3.3. 両者の比較
| 項目 | SQL Injection | ORM Injection |
|---|---|---|
| 攻撃対象 | 生 SQL 文字列 | ORM のクエリ API / Query Builder |
| 典型パターン | 文字列連結による WHERE / VALUES 汚染 |
whereRaw(), raw(), 動的ソート・フィルタなど |
| 必要な知識 | SQL 文法 | SQL + ORM の内部クエリの挙動 |
| 防御の基本 | プレースホルダ・パラメータバインド | 安全な ORM API の利用、ライブラリ更新、入力制御 |
4. ORM Injection を見つける視点
4.1. 典型的な危険メソッド
TryHackMe でも挙げられていた通り、各フレームワークには「強力だけど危険寄り」のメソッドがあります:
| Framework | ORM | 要注意メソッド例 |
|---|---|---|
| Laravel | Eloquent |
whereRaw(), DB::raw()
|
| Ruby on Rails | ActiveRecord | where("name = '#{input}'") |
| Django | Django ORM |
extra(), raw()
|
| Spring / Java | Hibernate |
createQuery() に文字列連結 |
| Node.js | Sequelize | sequelize.query() |
コードレビューでは、まずここにユーザー入力が直結していないかを確認するのが定石です。
4.2. 実際のアプリからフレームワークを推定する
- Cookie 名(
laravel_sessionなど) - エラーページの見た目・スタックトレース
- HTTP ヘッダ
- URL パターンやログイン画面のデザイン
などから「Laravel っぽい」「Django っぽい」と分かると、狙うべき ORM / Query Builder の候補が絞れます。
4.3. 入力テストとエラーメッセージ
1' のような値を投入してみて、
- SQLSTATE 〜 のエラーがそのまま出てくる
- スタックトレースに
Illuminate\Database\Connection等が見える
といった挙動があれば、「ORM 経由で生 SQL が壊れている」強いシグナルになります。
5. Laravel/Eloquent:弱い実装による ORM Injection
5.1. 脆弱なコード例(whereRaw)
public function searchBlindVulnerable(Request $request)
{
$users = [];
$email = $request->input('email');
$users = Admins::whereRaw("email = '$email'")->get();
if ($users) {
return view('user', ['users' => $users]);
} else {
return view('user', ['message' => 'User not found']);
}
}
問題点
- ユーザー入力
$emailをそのまま"email = '$email'"に埋め込み - それを
whereRaw()に渡しているので、実質「生 SQL に文字列連結」と同じ構造
攻撃者が
1' OR '1'='1
のような値を入れると、内部の SQL は概ね:
SELECT * FROM admins WHERE email = '1' OR '1'='1';
となり、条件は常に真。
結果として、全ユーザーのレコードが取得されてしまいます。
ORM を使っているにも関わらず、
whereRaw()でユーザー入力を直結 → 通常の SQL Injection と同じ
という状態です。
5.2. 安全な書き方(where でバインド)
public function searchBlindSecure(Request $request)
{
$email = $request->input('email');
$users = User::where('email', $email)->get();
if (isset($users) && count($users) > 0) {
return view('user', ['users' => $users]);
} else {
return view('user', ['message' => 'User not found']);
}
}
ここでは where('email', $email) を使っています。
- Eloquent は内部で
SELECT * FROM users WHERE email = ?
のような プレースホルダ付き SQL を生成し、 -
$emailは 値としてバインド されるだけ
そのため、どれだけ怪しい文字列を入れても「文字列」として扱われ、
SQL 構文は壊れません。
6. ライブラリ側の脆弱性を突く ORM Injection(Spatie Query Builder 例)
アプリの書き方だけでなく、利用しているライブラリの実装が弱い 場合も、ORM Injection の入口になります。
TryHackMe の例では、Laravel で Spatie の Query Builder を使い、以下のようなエンドポイントがあるとします:
GET /query_users?sort=name
- これは「
sortパラメータで並び替え」を行う機能 - 内部 SQL イメージ:
SELECT * FROM users ORDER BY name ASC LIMIT 2;
もしライブラリ側が「sort をそのまま ORDER BY に渡す」実装をしていた場合、
- 通常は
name/emailなどカラム名が来る前提 - しかし、攻撃者はそこに 特殊な文字列 を入れてクエリを壊しにいく
TryHackMe のシナリオでは、
-
MySQL の JSON 演算子
->(json_extractのシンタックスシュガー)を利用 -
ORDER BY句内で JSON 抽出を行っている形に見せかけつつ構文を抜け出し、 -
追加の
LIMIT 10などを差し込み、本来のLIMIT 2をバイパスするname-%3E%22%27))%20LIMIT%2010%23
というイメージの攻撃が紹介されています。
ここで重要なのは、実際のペイロード文字列そのものというよりも、
「並び替え・フィルタ用のパラメータを、そのまま ORDER BY に渡すと危険」
「ORM / Query Builder / サードパーティパッケージが内部でどう SQL を組み立てているか理解しないと、想定外のインジェクション経路が生まれる」
という点です。
7. 防御のためのベストプラクティス
ORM Injection の攻撃面が分かったところで、最後に守る側の実務チェックリストを整理します。
7.1. 入力バリデーション
- サーバ側(+必要ならクライアント側)で必ずバリデーション
- 型・文字種・長さ・フォーマットを厳密に制限
- 「メールアドレスのはずなのに
'や空白や制御文字が入っている」などを弾く
推奨:Allowlist(ホワイトリスト)方式
- 許可される値・パターンを明示して、それ以外は全部拒否する
- 特にソートキー、列名、フラグなどは
「サーバ側の配列から選択」する形にする
7.2. パラメータ化されたクエリ(Prepared Statements)
- ORM の標準 API(
where(),filter(),Queryset.filter()など)は、内部でパラメータバインドを行うものが多い - 生 SQL を書く場合も プレースホルダ+バインド を徹底
- 文字列連結でクエリを組み立てない
7.3. 危険メソッドの扱いポリシー
-
whereRaw(),DB::raw(),raw(),extra()など- 原則「ユーザー入力と組み合わせ禁止」
- やむを得ず使うなら:定数 or 完全ホワイトリスト済み値のみ
- コードレビューで「raw 系メソッド+入力値」の組み合わせを重点チェック
7.4. ORM / ライブラリのアップデート
- Doctrine / Hibernate / SQLAlchemy / Eloquent / Query Builder / Spatie 系などの
- バージョン
- セキュリティアドバイザリ(CVE / リリースノート)
を定期的に確認
- 「便利ライブラリを入れたら、そこが並び替えパラメータをそのままクエリに渡していた」
というパターンは実際に起こる
7.5. 各 ORM における安全なクエリ例
Doctrine (PHP)
$query = $entityManager->createQuery(
'SELECT u FROM User u WHERE u.username = :username'
);
$query->setParameter('username', $username);
$users = $query->getResult();
SQLAlchemy (Python)
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
user = session.query(User).filter_by(username=username).first()
Hibernate (Java)
String hql = "FROM User WHERE username = :username";
Query query = session.createQuery(hql);
query.setParameter("username", username);
List results = query.list();
Entity Framework (.NET)
var user = context.Users.FirstOrDefault(u => u.Username == username);
いずれも共通しているのは、
- SQL 文字列の中にユーザー入力を埋め込まない
- ORM / フレームワークの提供する パラメータバインド機構に任せる
という点です。
まとめ
- ORM は SQL を隠蔽してくれるが、
「使い方を間違えれば普通にインジェクションが起こる」 - ORM Injection は、
-
whereRaw()やraw()など「生 SQL ラッパー」 - ソート・フィルタ・検索などの動的パラメータ
- 脆弱なサードパーティ Query Builder
を通じて発生しうる
-
- 防御のポイントは:
- 安全な ORM API を優先して使う
- 危険メソッド+ユーザー入力の組み合わせを禁止
- パラメータバインドとホワイトリストバリデーション
- ライブラリの更新とコードレビュー・テスト