セキュリティリスク
データベースには機密データが含まれていることが多く、危険な操作を実行できます。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を使用します
- Presentersを使用し、
action*()
およびrender*()
メソッドのパラメータにデータ型を指定します - または、
filter_var()
などの標準的なPHPツールを使用して独自の検証層を実装します
カラムの安全な操作
前のセクションでは、パラメータ値を正しく検証する方法を示しました。ただし、SQLクエリで配列を使用する場合、そのキーにも同じ注意を払う必要があります。
// ❌ 危険なコード - 配列内のキーが処理されていません
$database->query('INSERT INTO users', $_POST);
INSERTおよびUPDATEコマンドの場合、これは重大なセキュリティエラーです –
攻撃者はデータベース内の任意のカラムを挿入または変更できます。たとえば、is_admin = 1
を設定したり、機密カラムに任意のデータを挿入したりできます(いわゆるマスアサインメント脆弱性)。
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']);