flawopen.com/SQL Injection in Java/MyBatis

MyBatis ${} vs #{}: which one is safe?

Reference page — draft, pending review
Short answer

#{value} is safe — it compiles to a bound JDBC parameter. ${value} is raw text substitution before the SQL is even parsed — it's exactly as dangerous as string concatenation, despite looking almost identical in the XML mapper.

Side by side

UNSAFE — ${} text substitution
<select id="getUser">
  SELECT * FROM users
  WHERE id = ${id}
</select>
// id is spliced into the SQL text
// before the statement is built
SAFE — #{} bound parameter
<select id="getUser">
  SELECT * FROM users
  WHERE id = #{id}
</select>
// compiles to a PreparedStatement
// with id bound as a parameter

Why MyBatis has both at all

${} isn't a bug in MyBatis — it exists for cases where you genuinely need to substitute something a bind parameter can't represent, like a table name or a column name in dynamic sorting (ORDER BY ${column}). The problem is exclusively when ${} is used for a value that could instead have been a bound parameter.

When you must use ${}

Table and column names need an allow-list, not escaping

Since ${} is unavoidable for identifiers, validate the value against a fixed allow-list of known-legitimate table/column names before it ever reaches the mapper — never accept an arbitrary string for this position.

How to check your codebase

grep -rn '\$\{' --include="*.xml" . | grep -i mapper
For each hit, confirm it's substituting an identifier (table/column) with an allow-list check upstream — if it's substituting a value that could be a bind parameter instead, that's the fix to make.

FAQ

Does this apply to annotation-based mappers too?

Yes — @Select("... WHERE id = ${id}") on a Java interface method has the exact same risk as the XML equivalent.

Is there a MyBatis-specific linter for this?

Not a dedicated one in common use — a CI grep for ${} in mapper files, reviewed manually against the allow-list rule above, is currently the practical check.

References