flawopen.com/SQL Injection/Java

SQL Injection in Java

Critical CWE-89 Draft — pending review
Language: English Português (Brasil) Español Français Deutsch Русский 简体中文 日本語 हिन्दी 한국어 Bahasa Indonesia
ELI5

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.

Key terms on this page
user-controlled input
Any value that ultimately came from whoever is using — or attacking — the app: a form field, a URL parameter, an uploaded filename, an HTTP header. The app can't assume it's well-formed or safe.
SQL query
The command sent to a database — e.g. "get this row," "delete this table." Its meaning comes entirely from its exact text, which is what makes injecting extra text into it dangerous.

What's happening

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.

Real-world impact

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.

Vulnerable vs. fixed

VULNERABLE
// userId comes straight from the request
String query = "SELECT * FROM users "
    + "WHERE id = " + userId;
Statement stmt =
    connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
FIXED
// 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();

Why the fix works

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.

Java-specific gotchas

MyBatis: ${} vs #{} is the whole ballgame

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.

Hibernate/JPA's HQL and JPQL are just as injectable as raw SQL

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.

Batch operations still need per-value binding

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.

Common misconceptions

"PreparedStatement is just for performance"

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.

"Hibernate means I'm using an ORM, so I'm safe"

True for its object-query API — false the moment a query string is built with + before being handed to createQuery().

"I validated the input format, so concatenation is fine"

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.

How to check if you're affected

grep -rn "createStatement()" --include="*.java" . grep -rn '\$\{' --include="*.xml" . | grep -i mapper grep -rn "createQuery(.*+" --include="*.java" .
A static analyzer like SpotBugs with the FindSecBugs plugin flags SQL injection patterns (rule SQL_INJECTION_JDBC and related) automatically in CI.

Prevention checklist

FAQ

Is Hibernate/JPA safe from SQL injection by default?

Its standard object-query methods are. Raw HQL/JPQL strings built with concatenation are not — the same discipline applies as with JDBC.

What's the difference between ${} and #{} in MyBatis?

#{} becomes a bound JDBC parameter. ${} is textual substitution before the query is even compiled — treat it as equivalent to string concatenation.

Does Spring Data JPA protect me automatically?

Its derived query methods and @Query with named parameters do. @Query(nativeQuery = true) built with concatenated strings does not.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: Command InjectionPath Traversal XSSInsecure Deserialization