SQLインジェクションとは
ユーザの入力値をSQLとして解釈してしまうプログラムによる脆弱性です。
SQLインジェクションを避ける原則
以下に書かれています。
実際は以下の2つでだいたい問題ありません。
- 言語やライブラリが提供するエスケープ機能を使用する。
- Javaはprepared statementを使う。
- 多くの場合パラメータをSQLと別に渡すことが可能なため、最も望ましい。
- ライブラリが実装しているエスケープ機能を使う場合もある。
- 逆に言えば、自前で実装しない。
- Javaはprepared statementを使う。
- prepared statementが使えない場合は「許可されたものの一覧表」を使用する
- 例: ORDER BYはASC, DESCのいずれか。
- 画面から’ASC’, ‘DESC’という文字列を渡す場合は、他の文字列が渡らないことを検証する。
- 実際は0 -> ASC, 1 -> DESCのように、SQLを連想させないほうがいいと思う。
- 参考: 僕が「ホワイトリスト」を採用しなかった訳 - ockeghem(徳丸浩)の日記
- 例: ORDER BYはASC, DESCのいずれか。
Javaでの例
基本
// 悪い例
String sql = "SELECT * FROM USER WHERE USER_ID = '" + userId + "'";
Statement stmt = conn.createStatement(sql);
// 良い例
String sql = "SELECT * FROM USER WHERE USER_ID = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, userId);
入力値によってWHEREが変わるもの
// 悪い例
// userIdはフォームで渡されたパラメータとする。
String sql = "SELECT * FROM USER ";
if (userId != null) {
sql += "WHERE USER_ID = '" + userId + "'";
}
Statement stmt = conn.createStatement(sql);
// 良い例
// SQLインジェクションではないですが、例が良くないのであとで書き換えます。。。
List<String> values = new ArrayList<>();
values.add(userId);
String sql = "SELECT * FROM USER ";
if (userId != null) {
sql += "WHERE USER_ID = ?";
}
PreparedStatement stmt = conn.prepareStatement(sql);
int i = 1;
for(String value: values) {
stmt.setString(i++, value);
}
IN句を作成する場合
// 悪い例
// userIdListはフォームで渡されたパラメータとする。
String sql = "SELECT * FROM USER WHERE USER_ID IN (";
for (String userId : userIdList) {
sql += "'" + userId + "',";
}
sql = sql.substring(0, sql.length() - 1);
Statement stmt = conn.createStatement(sql);
// 良い例
// SQLインジェクションではないですが、コードはもっと良いものにできますね。。。
List<String> values = new ArrayList<>();
String sql = "SELECT * FROM USER WHERE USER_ID IN (";
for (String userId : userIdList) {
sql += "?,";
values.add(userId);
}
sql = sql.substring(0, sql.length() - 1);
PreparedStatement stmt = conn.prepareStatement(sql);
int i = 1;
for(String value: values) {
stmt.setString(i++, value);
}
プリペアードステートメントでは対応できないもの
- ソート順序(ORDER BY ASC/DESC)
- 列名、テーブル名
ソート順序
ソート順序については2種類しかないため、条件分岐で対応可能です。
// 悪い例
String orderBy = ...; // 画面から渡され、通常はASC/DESCのどちらかが入る。
String sql = "SELECT * FROM USER ORDER BY" + orderBy;
// 良い例
String orderBy = ...; // 画面から渡され、通常はASC/DESCのどちらかが入る。
String sql = "SELECT * FROM USER ";
switch(orderBy) {
case "ASC":
sql += "ORDER BY ASC";
break;
case "DESC":
sql += "ORDER BY DESC";
break;
default:
// エラー処理
}
列名、テーブル名
ある一定の列名、テーブル名のみ使用する場合は、 その中に含まれているかどうかのチェックで対応可能です。
// 良い例
List<String> columns = ...; // 画面から渡され、列名が入る。
List<String> validColumns = Arrays.asList("NAME", "USER_ID", ...);
String sql = "SELECT ";
for(String column: columns) {
if (validColumns.contains(column) {
sql += column + ",";
}
}
sql = sql.substring(0, sql.length() - 1);
sql += " FROM USER";
もし、ある一定の列名、テーブル名というのが決められないのなら、 それはデータベース管理ソフトを除いて、設計に問題があります。
セカンドオーダーSQLインジェクション
そんなものはなかった。
正しい実装をしていれば起きません。