MyBatisにおける#{}と${}の違いとSQLインジェクション対策

MyBatisでSQLマップを使用してクエリを実行する際、動的にパラメータを渡す必要があることがよくあります。例えば、以下のようなSQL文があります:

SELECT * FROM users WHERE user_id = #{userId} AND user_name = '${userName}'

動的SQLの解析段階において、#{ }と${ }は異なる挙動を示します:

#{ }はJDBCのプリペアドステートメント(prepared statement)のパラメータマークとして「?」に変換されます。上記の例は以下のように解析されます:

...... user_id = ?

${ }は単なる文字列の置換に過ぎず、MyBatisの動的SQL解析段階で変数置換が行われます。例えば、userNameパラメータに"山田"が渡された場合:

...... user_name = '山田'

最終的に解析された完全なSQL文は以下のようになります:

SELECT * FROM users WHERE user_id = ? AND user_name = '山田'

そして、この解析済みのSQL文がJDBCを介して実行されます。

${ }はプリコンパイル前に変数置換が行われるため、SQLインジェクションの問題が発生する可能性があります。例えば、userNameパラメータに"1' OR '1'='1"が渡された場合:

MyBatisでの解析後、以下のようなSQL文になります:

SELECT * FROM users WHERE user_id = ? AND user_name = '1' OR '1'='1'

これにより、すべてのデータが検索されてしまいます。

ソースコードの確認:

MyBatisのクエリ実行時には、PreparedStatementHandler#queryが実行され、生成されたPreparedStatementを確認できます。

もしuserNameパラメータに"1' OR '1'='1"が渡された場合、SQLインジェクションが発生します。

では、${userName}を#{userName}に変更し、"1' OR '1'='1"を渡した場合はどうでしょうか?

MyBatisが解析した結果は以下のようになります:

データベースの実行ログで実行されたSQL文を確認すると、渡した文字列がエスケープされていることがわかります。

プリコンパイルを使用したプレースホルダ方式で値を渡すことで、特殊文字をエスケープし、SQLインジェクションを防止できます。

その理由は:JDBCのPreparedStatementを使用することで、SQL文:"SELECT * FROM users WHERE user_id = ? AND user_name = ?"が事前にコンパイルされるためです。つまり、SQLエンジンは事前に構文解析を行い、構文木を生成し、実行計画を作成します。その後、入力されるパラメータが何であっても、そのSQL文の構造には影響を与えません。構文解析は主にselect、from、where、and、or、order byなどのSQLコマンドを分析することであり、後からこれらのコマンドが入力されてもSQLコマンドとして実行されることはありません。なぜなら、これらのSQLコマンドを実行するには、まず構文解析を行い実行計画を生成する必要があるからです。構文解析が完了し、プリコンパイルされている場合、後から入力されるパラメータが絶対にSQLコマンドとして実行されることはなく、文字列リテラルパラメータとして扱われるだけです。したがって、SQLのプリコンパイルはSQLインジェクションを防御できます。また、同じSQLを複数回実行する際に効率が向上します。理由はSQLがコンパイル済みであるため、再度実行する際に再コンパイルする必要がないからです。

デフォルトではPreparedStatementでプリコンパイルを実行することはできず、URLにuseServerPrepStmts=trueパラメータを指定する必要があります(MySQL Server 4.1以前のバージョンはプリコンパイルをサポートしていませんが、Connector/J 5.0.5以降のバージョンでは、デフォルトでプリコンパイル機能は有効になっていません)。 例:jdbc:mysql://localhost:3306/testdb?useServerPrepStmts=true これにより、MySQLドライバがまずSQL文をサーバーに送信してプリコンパイルし、executeQuery()実行時にはパラメータのみをサーバーに送信することが保証されます。

タグ: MyBatis SQLインジェクション PreparedStatement JDBC パラメータバインディング

6月1日 23:31 投稿