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>
  • userId and status come from the request or business layer. With #{}, the SQL is WHERE 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 orderBy into ${}. Use a whitelist or fixed column names.

Example 3: Safe dynamic ORDER BY with whitelist

Java
private 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 orderBy and orderDir are 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 use LIKE #{pattern}. Never use ${} for search keywords.
SyntaxBehaviorInjection-safe?Use for
#{x}Placeholder, parameter bindingYesAll values (id, keyword, status, pagination, etc.)
${x}String substitution in SQLNoOnly code-controlled fragments (table name, whitelisted column)

Core Mechanism / Behavior

  • #{}: MyBatis generates ? and calls PreparedStatement.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 use CONCAT('%', #{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 #{}.