セキュリティリスク

データベースには機微なデータが入っていることが多く、危険な操作も行えます。Nette Database を安全に使うには、次のことが欠かせません。

  • 安全な API と危険な API の違いを理解する
  • パラメータ化されたクエリを使う
  • 入力データをきちんと検証する

SQL インジェクションとは何か

SQL インジェクションはデータベースを扱ううえでもっとも深刻なセキュリティリスクです。これは検査されていないユーザーの入力が SQL のクエリの一部になったときに起こります。攻撃者は自分の SQL のコマンドを差し込んで、次のことができてしまいます。

  • データへの許されないアクセスを得る
  • データベースのデータを書き換えたり消したりする
  • 認証をすり抜ける
// ❌ 危険なコード - SQL インジェクションに対して脆弱です
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");

// 攻撃者は次のような値を入れるかもしれません: ' OR '1'='1
// できあがるクエリは: SELECT * FROM users WHERE name = '' OR '1'='1'
// これはすべてのユーザーを返します

同じことが Database Explorer にも当てはまります。

// ❌ 危険なコード - SQL インジェクションに対して脆弱です
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");

パラメータ化されたクエリ

SQL インジェクションに対する基本の守りはパラメータ化されたクエリです。Nette Database はその使い方をいくつか用意しています。

もっとも単純なのは疑問符のプレースホルダを使うことです。

// ✅ 安全なパラメータ化されたクエリ
$database->query('SELECT * FROM users WHERE name = ?', $name);

// ✅ Explorer での安全な条件
$table->where('name = ?', $name);

これは、疑問符のプレースホルダとパラメータで式を入れられる Database Explorerのほかのすべてのメソッドにも当てはまります。

INSERT、UPDATE のコマンドや WHERE 句では、値を配列で渡せます。

// ✅ 安全な INSERT
$database->query('INSERT INTO users', [
	'name' => $name,
	'email' => $email,
]);

// ✅ Explorer での安全な INSERT
$table->insert([
	'name' => $name,
	'email' => $email,
]);

パラメータの値の検証

パラメータ化されたクエリは、安全にデータベースを扱うための土台です。とはいえ、そこに入れる値はいくつかの段階の検査を経なければなりません。

型の検査

もっとも大事なのはパラメータのデータ型が正しいことを確かめることです。これは Nette Database を安全に使うために欠かせない条件です。データベースは、入力データがすべてその列に対応する正しいデータ型を持っていると前提にしています。

たとえば前の例の $name が思いがけず文字列ではなく配列だったとすると、Nette Database はその要素をすべて SQL のクエリに入れようとして、エラーになります。ですから $_GET$_POST$_COOKIE からの検証していないデータを、データベースのクエリで直接決して使わないでください

書式の検証

2 つめの段階ではデータの書式を確かめます。たとえば文字列が UTF-8 の文字コードで、長さが列の定義に合っているか、数値がその列のデータ型で許される範囲に収まっているか、といったことです。

この段階の検証はデータベース自身にある程度は任せられます。多くのデータベースは正しくないデータを拒みます。とはいえ振る舞いはさまざまで、長い文字列を黙って切り詰めたり、範囲の外の数を丸めたりするものもあります。

ドメインに固有の検証

3 つめの段階は、あなたのアプリケーションに固有の論理的な検査です。たとえば選択肢の値が提示されたものと一致するか、数が思ったとおりの範囲にあるか(年齢 0〜150 歳など)、値どうしの依存関係が筋の通ったものかを確かめます。

おすすめの検証の方法

  • すべての入力の適切な検証を自動的に受け持つ Nette Formsを使います。
  • プレゼンターを使い、action*()render*() メソッドのパラメータにデータ型を指定します。
  • あるいは filter_var() のような PHP の標準の道具で独自の検証の層を作ります。

列を安全に扱う

前の節では、パラメータの値をきちんと検証する方法を見ました。しかし SQL のクエリで配列を使うときは、そのキーにも同じだけ気を配らなければなりません。

// ❌ 危険なコード - 配列のキーが検査されていません
$database->query('INSERT INTO users', $_POST);

INSERT と UPDATE のコマンドでは、これは致命的なセキュリティの欠陥です。攻撃者はデータベースのどの列でも挿入したり書き換えたりできてしまいます。たとえば is_admin = 1 を設定したり、機微な列に好きなデータを入れたりできます(いわゆる Mass Assignment Vulnerability)。

WHERE の条件ではさらに危険です。演算子を含められるからです。

// ❌ 危険なコード - 配列のキーが検査されていません
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// クエリ WHERE (`salary` > 100000) が実行されます

攻撃者はこのやり方で従業員の給与を順に探り当てられます。たとえばまず 100,000 より上の給与を問い合わせ、次に 50,000 より下を問い合わせ、範囲を少しずつ狭めていくことで、全員のおおよその給与を暴けます。この種の攻撃を SQL 列挙と呼びます。

where()whereOr() メソッドはさらにずっと柔軟で、キーと値の中で演算子や関数を含む SQL の式に対応しています。これは攻撃者に SQL インジェクションの余地を与えます。

// ❌ 危険なコード - 攻撃者が自分の SQL を差し込めます
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// クエリ WHERE (0) UNION SELECT name, salary FROM users WHERE (1) が実行されます

この攻撃は 0) でもとの条件を終わらせ、UNION で自分の SELECT を継ぎ足して users テーブルの機微なデータを得て、WHERE (1) で構文的に正しいクエリとして閉じています。

列のホワイトリスト

列の名前を安全に扱うには、ユーザーが許された列だけを扱えて、自分で列を足せないようにするしくみが要ります。危険な列の名前を見つけて遮る(ブラックリスト)こともできますが、この手はあてになりません。攻撃者はこちらが思いつかなかった、危険な列の名前の新しい書き方をいつでも編み出せるからです。

ですから発想を逆にして、許される列のはっきりした一覧(ホワイトリスト)を定義するほうがずっと安全です。

// ユーザーが変えてよい列
$allowedColumns = ['name', 'email', 'active'];

// 入力から許されていない列をすべて取り除きます
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));

// ✅ これでクエリで安全に使えます。たとえば次のようにです:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);

動的な識別子

テーブルや列の名前を動的に扱うには ?name のプレースホルダを使います。これはそのデータベースの構文に従って識別子をきちんとエスケープします(MySQL ならバッククォートを使います)。

// ✅ 信頼できる識別子の安全な使い方
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// MySQL での結果: SELECT `name` FROM `users`

大事な点として、?name の記号はアプリケーションのコードで定義した信頼できる値にだけ使ってください。ユーザーから来る値には、やはりホワイトリストを使います。さもないとセキュリティリスクにさらされます。

// ❌ 危険 - ユーザーの入力を決して使わないでください
$database->query('SELECT ?name FROM users', $_GET['column']);
バージョン: 4.x