Mybatisを利用して、SQLを組み立てる際は、SQL Injectionが発生しないように注意する必要があります。特にSQLに値を埋め込みたいとき、「${…}」を使うべきか、「#{…}」を使うべきかを意識しない方はいると思いますが、実は「${…}」を使う場合はかなり危険です。
基本的に、必ずアプリケーションとして安全な値であることを担保した上で、テーブル名、カラム名、ソート条件などに限定して「${…}」を使用しますが、その以外の場合は基本的に「#{…}」を使うべきです。
バインド変数と置換変数
MyBatis3では、SQLに値を埋め込む仕組みとして、以下の2つの方法を提供している。
項番 | 方法 | 説明 |
1 | バインド変数(#{…})を使用して埋め込む | この方法を使用すると、 SQL組み立て後にjava.sql.PreparedStatement を使用して値が埋め込められるため、安全に値を埋め込むことができる。ユーザからの入力値をSQLに埋め込む場合は、原則バインド変数を使用すること。 |
2 | 置換変数(${…})を使用して埋め込む | この方法を使用すると、SQLを組み立てるタイミングで文字列として置換されてしまうため、安全な値の埋め込みは保証されない。 |
MyBatisのデフォルトではJavaのPreparedStatement
を利用してSQLを実行します。実はバインド変数#{…}
はPreparedStatement
のパラメータとして設定されます。つまり以下のようなパラメータが利用できる箇所でのみ、利用することができます。
・SELECT文のWHERE句における値
・INSERT文のVALUES句における値
・UPDATE文のSET句における値、WHERE句における値
・DELETE文のWHERE句における値
バインド変数を使用する例は以下です。
<insert id="create" parameterType="Example">
INSERT INTO
t_example
(
item_id,
item_title,
created_time
)
VALUES
(
#{itemId},
#{itemTitle},
#{createdTime}
)
</insert>
置換変数を使用する例は以下です。
public interface TodoRepository {
List<Todo> findAll(@Param("direction") String direction);
}
<select id="findAll" parameterType="String" resultType="Todo">
SELECT
*
FROM
t_todo
ORDER BY
todo_id ${direction}
</select>
なぜ置換変数は危険なの?
以下の例を考えてみましょう。
元々itemnameを指定して、特定のレコードを絞り込んで取得したいが、itemnameの値を「’name’ OR 1=1;」と指定したら、itemsのすべてのレコードを取得してしまうことになります。
更に、itemnameの値を「’name’; DELETE * FROM items;」と指定したら、itemsテーブルの値をすべて削除されてしまいます。
SELECT *
FROM items
WHERE itemname = ${itemName}
対策
置換変数を使用する場合、必ず代入の値をチェックもしくは制限するようにします。
例えば、コード値とSQLに埋め込むための値のペアをMap
に格納しておき
Map<String, String> directionMap = new HashMap<String, String>();
directionMap.put("1", "ASC");
directionMap.put("2", "DESC");
入力値はコード値として扱い、SQLを実行する処理の中で安全な値に変換することが望ましい。
String direction = directionMap.get(directionCode);
todoRepository.findAllByCriteria(criteria, direction);
コメント