FlawOpen

flawopen.com/SQL Injection in Java/MyBatis

MyBatis ${} vs #{}: prevent SQL injection

Reference page
Short answer

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

UNSAFE — ${} raw text
<select id="getUser">
  SELECT * FROM users
  WHERE id = ${id}
</select>
// Raw substitution: the SQL text changes
BOUND VALUE — #{}
<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

Allowlist the complete SQL identifier or fragment

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' .
Review every hit in XML and annotation-based mappers. Trace the substituted value to its source. Use #{} for values; for ${}, confirm the entire identifier or SQL fragment comes from a finite server-side allowlist. A text search finds syntax, not every indirect mapper construction.

FAQ

Does this apply to annotation-based mappers?

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.

Is a search for ${} enough to prove the code is safe?

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.

References

See also SQL Injection (CWE-89): How It Works and How to Prevent It in Every Language — flawopen.com MySQLi vs PDO: Secure Prepared Statements Is NHibernate Safe from SQL Injection? MongoDB NoSQL Injection: Safe Query Filters Why String Formatting Risks Numeric ID Queries What Is a Parameterized Query? Is Prisma $queryRaw Safe from SQL Injection?