flawopen.com/SQL Injection/Java
Imagine a form that only ever expects a ticket number, like 482. SQL injection is what happens when someone types something sneaky into that box instead of a number — a trick phrase that makes the system say "show me every ticket" instead of just ticket 482, because the system never checked that what it received was actually just a number.
SQL injection happens when user-controlled input gets inserted directly into a database query's text, instead of being passed as a separate value. In Java, this shows up whenever a query is built with plain Statement and string concatenation instead of PreparedStatement — and the same failure appears one layer up, in ORM query languages like HQL and JPQL, when they're built the same way.
In 2015, UK telecom TalkTalk suffered a breach affecting over 150,000 customers after attackers exploited a SQL injection flaw in a legacy web page inherited through a company acquisition. The UK's data protection regulator fined TalkTalk £400,000, describing the failure as preventable and basic.
Source: UK Information Commissioner's Office enforcement notice, 2016 — see References below.// userId comes straight from the request
String query = "SELECT * FROM users "
+ "WHERE id = " + userId;
Statement stmt =
connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
// value is bound, never concatenated
String query =
"SELECT * FROM users WHERE id = ?";
PreparedStatement stmt =
connection.prepareStatement(query);
stmt.setString(1, userId);
ResultSet rs = stmt.executeQuery();
PreparedStatement sends the query's structure to the database once, then binds the value separately through setString() — the database never re-parses the value as part of the SQL text. This is a different class from Statement, not just a performance optimization on top of it; the class itself is what marks the safety boundary.
In MyBatis XML mappers, #{value} compiles to a bound parameter — safe. ${value} is raw string substitution into the SQL text before it's even sent — exactly as dangerous as hand-written concatenation, and easy to reach for out of habit since the syntax looks almost identical.
session.createQuery("FROM User WHERE id = " + id) is vulnerable in exactly the same way as a concatenated Statement — the ORM layer doesn't protect you unless you use setParameter() with a named or positional placeholder.
addBatch() with a PreparedStatement is safe as long as each row's values go through setX() calls — building each batched query string manually reintroduces the same risk N times over.
Query plan caching is a real side benefit, but the primary reason to use it is parameter binding — the safety property doesn't come from caching.
True for its object-query API — false the moment a query string is built with + before being handed to createQuery().
Format validation and query-construction safety are separate concerns — a validation bug or an edge case you didn't anticipate reopens the exact same hole.
grep -rn "createStatement()" --include="*.java" .
grep -rn '\$\{' --include="*.xml" . | grep -i mapper
grep -rn "createQuery(.*+" --include="*.java" .
Its standard object-query methods are. Raw HQL/JPQL strings built with concatenation are not — the same discipline applies as with JDBC.
#{} becomes a bound JDBC parameter. ${} is textual substitution before the query is even compiled — treat it as equivalent to string concatenation.
Its derived query methods and @Query with named parameters do. @Query(nativeQuery = true) built with concatenated strings does not.