0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【セキュリティ】ORM Injection 入門:ORM でもインジェクションは防げない

Posted at

はじめに

近年はセキュリティ意識の高まりから、多くの開発者が「生 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 = 1
  • SELECT * FROM users
  • SELECT * 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 を優先して使う
    • 危険メソッド+ユーザー入力の組み合わせを禁止
    • パラメータバインドとホワイトリストバリデーション
    • ライブラリの更新とコードレビュー・テスト

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?