How to Prevent SQL Injection in Java: JDBC, MyBatis, JPA & Hibernate Best Practices
This article reviews Java persistence technologies, shows how raw string concatenation in JDBC, MyBatis, JPA/Hibernate and native SQL can lead to SQL injection, and provides concrete safe‑coding patterns such as PreparedStatement, #{ } bindings, named parameters, and dynamic‑SQL safeguards.
Overview
SQL injection occurs when user‑supplied data is concatenated into a SQL string and sent to the database. In Java persistence layers the risk appears in plain JDBC, MyBatis (XML/annotation), JPA/Hibernate (HQL or native SQL) and native queries. The safe pattern is to separate code from data by using parameterised statements or by validating any fragment that must be inserted literally.
JDBC
Vulnerable code builds the statement with string concatenation:
String sql = "SELECT * FROM users WHERE name ='" + name + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);Secure code uses PreparedStatement with ? placeholders. The statement is prepared before any user data is added, so the database parses the SQL once and later binds the values as data, preventing re‑parsing of malicious input.
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, name);
ResultSet rs = ps.executeQuery();Preparing a statement after the SQL string has already been built does not remove the injection risk.
MyBatis
MyBatis maps Java methods to SQL defined in XML or annotations. Two placeholder syntaxes exist: #{} – generates a PreparedStatement and binds the value safely. ${} – performs literal string substitution and can re‑introduce injection.
Safe XML mapping (parameter bound with #{} ) :
<select id="getById" resultType="org.example.User">
SELECT * FROM user WHERE id = #{id}
</select>Vulnerable XML mapping (literal substitution with ${} ) :
<select id="getByName" resultType="org.example.User">
SELECT * FROM user WHERE name = '${name}' LIMIT 1
</select>When a fragment such as an ORDER BY column cannot be bound with #{}, apply a whitelist before substitution. MyBatis provides <choose> / <when> / <otherwise> to implement the whitelist:
<select id="getUserListSortBy" resultType="org.example.User">
SELECT * FROM user
<choose>
<when test="sortBy == 'name' or sortBy == 'email'">
ORDER BY ${sortBy}
</when>
<otherwise>
ORDER BY name
</otherwise>
</choose>
</select>Other dynamic constructs such as LIKE with wildcards, IN clauses using <foreach>, and pagination with LIMIT should also use #{} for values and whitelist any raw fragments.
JPA & Hibernate
Both APIs expose HQL/JPQL (object‑oriented query language) and native SQL. Concatenating parameters into the query string is vulnerable:
Query<User> q = session.createQuery(
"from User where name = '" + name + "'"
);
User u = q.getSingleResult();Secure alternatives:
Positional parameter: ? with setParameter(int, value).
Named parameter: :name with setParameter("name", value).
Parameter list for IN clauses using setParameterList.
Bean property binding with setProperties(bean).
// Positional
Query<User> q = session.createQuery(
"from User where name = ?", User.class);
q.setParameter(0, name);
// Named
Query<User> q2 = session.createQuery(
"from User where name = :name", User.class);
q2.setParameter("name", name);
// List parameter
Query<User> q3 = session.createQuery(
"from User where name in (:nameList)", User.class);
q3.setParameterList("nameList",
Arrays.asList("lisi", "zhaowu"));
// Bean binding
User bean = new User();
bean.setName("zhaowu");
Query<User> q4 = session.createQuery(
"from User where name = :name", User.class);
q4.setProperties(bean);Native SQL via JPA/Hibernate
Native queries suffer the same injection risk as plain JDBC. Use named parameters with createNativeQuery:
String sql = "select * from user where name = :name";
Query query = session.createNativeQuery(sql);
query.setParameter("name", name);Key mitigation strategies
Never concatenate untrusted input into SQL strings.
Prefer PreparedStatement, MyBatis #{}, or JPA named/positional parameters.
When a literal fragment (e.g., column name, ORDER BY) must be inserted, validate it against a whitelist or use an enum‑based mapping.
For LIKE patterns, add wildcards to the bound value (e.g., ps.setString(1, "%" + term + "%")) or use MyBatis <bind> to construct the pattern safely.
Use <foreach> with #{} for IN lists to avoid manual string joining.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
