セキュリティリスク
*********
<div class=perex>
データベースには機微なデータが入っていることが多く、危険な操作も行えます。Nette Database を安全に使うには、次のことが欠かせません。
- 安全な API と危険な API の違いを理解する
- パラメータ化されたクエリを使う
- 入力データをきちんと検証する
</div>
SQL インジェクションとは何か
================
SQL インジェクションはデータベースを扱ううえでもっとも深刻なセキュリティリスクです。これは検査されていないユーザーの入力が SQL のクエリの一部になったときに起こります。攻撃者は自分の SQL のコマンドを差し込んで、次のことができてしまいます。
- データへの許されないアクセスを得る
- データベースのデータを書き換えたり消したりする
- 認証をすり抜ける
```php
// ❌ 危険なコード - SQL インジェクションに対して脆弱です
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// 攻撃者は次のような値を入れるかもしれません: ' OR '1'='1
// できあがるクエリは: SELECT * FROM users WHERE name = '' OR '1'='1'
// これはすべてのユーザーを返します
```
同じことが Database Explorer にも当てはまります。
```php
// ❌ 危険なコード - SQL インジェクションに対して脆弱です
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
```
パラメータ化されたクエリ
============
SQL インジェクションに対する基本の守りはパラメータ化されたクエリです。Nette Database はその使い方をいくつか用意しています。
もっとも単純なのは**疑問符のプレースホルダ**を使うことです。
```php
// ✅ 安全なパラメータ化されたクエリ
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Explorer での安全な条件
$table->where('name = ?', $name);
```
これは、疑問符のプレースホルダとパラメータで式を入れられる [Database Explorer|explorer]のほかのすべてのメソッドにも当てはまります。
INSERT、UPDATE のコマンドや WHERE 句では、値を配列で渡せます。
```php
// ✅ 安全な 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|forms:]を使います。
- [プレゼンター|application:]を使い、`action*()` と `render*()` メソッドのパラメータにデータ型を指定します。
- あるいは `filter_var()` のような PHP の標準の道具で独自の検証の層を作ります。
列を安全に扱う
=======
前の節では、パラメータの値をきちんと検証する方法を見ました。しかし SQL のクエリで配列を使うときは、そのキーにも同じだけ気を配らなければなりません。
```php
// ❌ 危険なコード - 配列のキーが検査されていません
$database->query('INSERT INTO users', $_POST);
```
INSERT と UPDATE のコマンドでは、これは致命的なセキュリティの欠陥です。攻撃者はデータベースのどの列でも挿入したり書き換えたりできてしまいます。たとえば `is_admin = 1` を設定したり、機微な列に好きなデータを入れたりできます(いわゆる Mass Assignment Vulnerability)。
WHERE の条件ではさらに危険です。演算子を含められるからです。
```php
// ❌ 危険なコード - 配列のキーが検査されていません
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// クエリ WHERE (`salary` > 100000) が実行されます
```
攻撃者はこのやり方で従業員の給与を順に探り当てられます。たとえばまず 100,000 より上の給与を問い合わせ、次に 50,000 より下を問い合わせ、範囲を少しずつ狭めていくことで、全員のおおよその給与を暴けます。この種の攻撃を SQL 列挙と呼びます。
`where()` と `whereOr()` メソッドは[さらにずっと柔軟で |explorer#where()]、キーと値の中で演算子や関数を含む SQL の式に対応しています。これは攻撃者に SQL インジェクションの余地を与えます。
```php
// ❌ 危険なコード - 攻撃者が自分の 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)` で構文的に正しいクエリとして閉じています。
列のホワイトリスト
---------
列の名前を安全に扱うには、ユーザーが許された列だけを扱えて、自分で列を足せないようにするしくみが要ります。危険な列の名前を見つけて遮る(ブラックリスト)こともできますが、この手はあてになりません。攻撃者はこちらが思いつかなかった、危険な列の名前の新しい書き方をいつでも編み出せるからです。
ですから発想を逆にして、許される列のはっきりした一覧(ホワイトリスト)を定義するほうがずっと安全です。
```php
// ユーザーが変えてよい列
$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 ならバッククォートを使います)。
```php
// ✅ 信頼できる識別子の安全な使い方
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// MySQL での結果: SELECT `name` FROM `users`
```
大事な点として、`?name` の記号はアプリケーションのコードで定義した信頼できる値にだけ使ってください。ユーザーから来る値には、やはり[ホワイトリスト |#列のホワイトリスト]を使います。さもないとセキュリティリスクにさらされます。
```php
// ❌ 危険 - ユーザーの入力を決して使わないでください
$database->query('SELECT ?name FROM users', $_GET['column']);
```
セキュリティリスク
データベースには機微なデータが入っていることが多く、危険な操作も行えます。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']);