Parameter Binding & SQL Injection Safety
In MyBatis, #{} uses PreparedStatement placeholders: parameters are bound safely and escaped, so they do not become part of the SQL text. ${} performs string substitution and injects the value directly into the SQL. Use ${} only for trusted, non-user-controlled fragments (e.g. table names, ORDER BY columns). This article explains the difference, correct usage, and risks with examples and a reference table.
Overview
- #{}: Produces a
?placeholder; the value is bound via JDBC with proper escaping. Use for all user input or untrusted data (ids, search terms, status, pagination, etc.). Default choice. - ${}: Replaces the expression with literal text in the SQL. No escaping. If user input reaches
${}, SQL injection is possible. Use only for fully code-controlled fragments (e.g. whitelisted column names, table names). - Rule: User input, request params, any untrusted data → #{} only. For dynamic table/column names or ORDER BY, use a whitelist before
${}, or avoid${}by using application-level branching (different methods for different columns).
Example
Example 1: Safe — #{} for query conditions
XML<select id="selectByUser" resultType="Order"> SELECT * FROM orders WHERE user_id = #{userId} AND status = #{status} </select>
userIdandstatuscome from the request or business layer. With #{}, the SQL isWHERE user_id = ? AND status = ?and parameters are bound separately. Safe.
Example 2: Dangerous — user input in ${}
XML<!-- DANGEROUS: if orderBy = "id; DROP TABLE orders--" this causes injection --> <select id="select" resultType="Order"> SELECT * FROM orders ORDER BY ${orderBy} </select>
- Never pass user-controlled
orderByinto${}. Use a whitelist or fixed column names.
Example 3: Safe dynamic ORDER BY with whitelist
Javaprivate static final Set<String> ALLOWED_ORDER = Set.of("id", "created_at", "amount"); String orderBy = ALLOWED_ORDER.contains(param.getOrderBy()) ? param.getOrderBy() : "id"; String orderDir = "DESC".equalsIgnoreCase(param.getOrderDir()) ? "DESC" : "ASC"; mapper.selectWithOrder(orderBy, orderDir);
XML<select id="selectWithOrder" resultType="Order"> SELECT * FROM orders ORDER BY ${orderBy} ${orderDir} </select>
- Only use this when
orderByandorderDirare strictly validated (e.g. whitelist). Prefer<choose>in XML with fixed column names and no${}for user-driven values.
Example 4: Safer — <choose> with no ${}
XML<select id="selectWithOrder" resultType="Order"> SELECT * FROM orders ORDER BY <choose> <when test="orderBy == 'created_at'">created_at</when> <when test="orderBy == 'amount'">amount</when> <otherwise>id</otherwise> </choose> ${orderDir} </select>
- Column name is chosen from a fixed set. For
orderDir, you still need a whitelist (e.g. "ASC"/"DESC") if passed from the user.
Example 5: LIKE with #{}
XML<select id="search" resultType="Order"> SELECT * FROM orders WHERE status LIKE CONCAT('%', #{keyword}, '%') </select>
- Or pass
"%"+keyword+"%"from Java and useLIKE #{pattern}. Never use${}for search keywords.
| Syntax | Behavior | Injection-safe? | Use for |
|---|---|---|---|
| #{x} | Placeholder, parameter binding | Yes | All values (id, keyword, status, pagination, etc.) |
| ${x} | String substitution in SQL | No | Only code-controlled fragments (table name, whitelisted column) |
Core Mechanism / Behavior
- #{}: MyBatis generates
?and callsPreparedStatement.setXxx(index, value). The driver handles escaping and types; the value is never interpreted as SQL. - ${}: Replaced during SQL parsing. The value becomes part of the SQL. Characters like
',;,--can alter semantics and enable injection or syntax errors. - LIKE: Use #{}; put
%in the parameter or useCONCAT('%', #{keyword}, '%'). Do not use${}for the pattern.
Key Rules
- Default to #{}; use it for any value from users or untrusted sources.
- ${} only for fully code-controlled SQL fragments. If you must use it, validate against a whitelist and never pass raw user input.
- Prefer application-level branching or whitelist mapping for dynamic table/column names to minimize use of
${}.
What's Next
See Dynamic SQL for <if> and <choose> with #{}. All condition parameters in N+1 and ResultMap queries should use #{}.