flawopen.com/SQL Injection in Java/MyBatis
MyBatis ${} vs #{}: prevent SQL injection
Think of #{value} as a sealed value slot in a prepared statement: the database treats input as data. ${value} copies text into the SQL statement before it is prepared. That difference matters whenever request data can reach the mapper.
Bound values vs raw SQL text
<select id="getUser">
SELECT * FROM users
WHERE id = ${id}
</select>
// Raw substitution: the SQL text changes
<select id="getUser">
SELECT * FROM users
WHERE id = #{id}
</select>
// MyBatis creates a PreparedStatement parameter
Why MyBatis provides both syntaxes
#{} binds a value using a JDBC PreparedStatement parameter. A parameter cannot stand for SQL syntax such as a table name, column name, or sort direction. MyBatis ${} performs direct text substitution for those dynamic fragments and does not escape the substituted text. Prefer a finite set of static statements or <choose> branches. If raw substitution is necessary, select the complete identifier or fragment from a server-side allowlist; never interpolate arbitrary request text.
When dynamic identifiers are required
A bound parameter cannot replace an identifier. For sorting, map a user choice such as “name” or “created” to fixed SQL fragments in server code (or use <choose> branches). Do not pass a client-supplied column, table name, or ASC/DESC token directly to ${}. Escaping or quoting alone does not make arbitrary SQL fragments safe.
How to audit MyBatis mappers
rg -n -F '${' --glob '*.xml' --glob '*.java' .FAQ
Yes. MyBatis uses the same substitution rules in annotations. A ${column} expression in @Select inserts raw text; bind data values with #{value} and allowlist any dynamic identifier.
No. Search results need review, and a text search can miss SQL assembled elsewhere or generated by helper code. Add review rules or static checks for mapper XML and annotations, then test that user-controlled sort choices map only to expected fixed fragments.