辞書: SQLインジェクション

投稿日:

SQLインジェクションとは

ユーザの入力値をSQLとして解釈してしまうプログラムによる脆弱性です。

SQLインジェクションを避ける原則

以下に書かれています。

実際は以下の2つでだいたい問題ありません。

  • 言語やライブラリが提供するエスケープ機能を使用する。
    • Javaはprepared statementを使う。
      • 多くの場合パラメータをSQLと別に渡すことが可能なため、最も望ましい。
    • ライブラリが実装しているエスケープ機能を使う場合もある。
      • 逆に言えば、自前で実装しない。
  • prepared statementが使えない場合は「許可されたものの一覧表」を使用する

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インジェクション

そんなものはなかった。

正しい実装をしていれば起きません。