CWE-564: SQL Injection: Hibernate
Overview
An ORM changes the API, not the trust boundary. Session.createQuery() and
createNativeQuery() both accept a plain string, and a string built by
concatenating request data carries exactly the injection it would in raw JDBC -
the database receives generated SQL with no idea an ORM produced it.
Hibernate findings are distinctive in where the concatenation survives. Entity
loads and derived repository methods are parameterised for you, so the remaining
hand-written queries are the awkward ones: dynamic sorting, IN clauses over a
variable list, and search filters assembled from optional fields. Only the first
of those is beyond parameter binding; the other two have safe APIs -
setParameterList and the Criteria API - that the concatenated code did not
use.
Relationship to Other CWEs
CWE-564 is the Hibernate-specific case of CWE-89 (SQL Injection) - the root cause and the fix are identical, only the API surface differs. If the finding does not involve Hibernate, use the CWE-89 page, which also carries Java guidance for plain JDBC.
OWASP Classification
A05:2025 - Injection
Risk
Critical: Successful injection reads, modifies or deletes anything the
application's database user can reach, and commonly enables authentication
bypass by making a lookup match a row it should not. HQL is translated to SQL
before execution, so union selects and boolean or time-based blind extraction
apply unchanged. Stacked statements do not: Hibernate's HQL parser rejects a
; outright, long before any driver setting is consulted, so a probe that ends
in ; DROP TABLE comes back as a syntax error and proves nothing about whether
the sink is injectable. Those belong to the native query sink below, where a
multi-statement driver will run them. The distinction also bounds the damage:
an injection into an HQL SELECT reads, and mutation needs the native sink or
an injection point inside an HQL update or delete.
Common Vulnerable Patterns
Concatenation into HQL
// VULNERABLE - the value becomes part of the query text
String hql = "FROM User WHERE username = '" + username + "'";
Query<User> query = session.createQuery(hql, User.class);
// Attack: username = admin' OR '1'='1
// Becomes: FROM User WHERE username = 'admin' OR '1'='1' -> every user
Why this is vulnerable: The quote in the input closes the literal the developer opened, and everything after it is parsed as query syntax. HQL is not a sandbox: it compiles to SQL against the same database, so the outcome is a SQL injection.
Native queries treated as a special case
// VULNERABLE - "native" is not a safety property
String sql = "SELECT * FROM orders WHERE user_id = " + userId;
Query<Order> query = session.createNativeQuery(sql, Order.class);
// Attack: userId = "1 OR 1=1" -> returns every order, not just this user's
Why this is vulnerable: createNativeQuery() accepts the same :name and
?1 parameter syntax as HQL, so there is no technical reason for a native query
to be concatenated. Numeric values are the usual excuse: with no quotes to
escape, the concatenation looks safe. An unquoted numeric position accepts an
entire boolean expression.
Identifiers and lists
// VULNERABLE - a bound parameter cannot be a column name
String hql = "FROM User ORDER BY " + sortColumn;
// Attack: sortColumn = "CASE WHEN substring((SELECT u2.password FROM User u2
// WHERE u2.id=1),1,1)='s' THEN id ELSE -id END"
// -> blind extraction: the row order flips when the guess is right.
// Both CASE branches must have the same type - mixing id with a
// string column makes the engine coerce the whole expression and
// the query errors instead of leaking
// VULNERABLE - list joined into the query text
String hql = "FROM User WHERE id IN (" + idList + ")";
// Attack: idList = "1) OR (1=1" -> every user
Why this is vulnerable: Parameter binding covers values, not syntax. An
identifier cannot be bound at all, which is why developers concatenate there.
Hibernate will accept ORDER BY :sortCol and render order by ?, and the
engine then rejects it - H2 reads the placeholder as an ordinal and raises a
number-format error, HSQLDB asks for a cast - so do not expect a query that
quietly returns unsorted rows. An ORDER BY expression is rich
enough to leak data one character at a time, even when the response body shows
only row order. The IN case is the same mistake in a place where a binding API
does exist and was not used.
Secure Patterns
Bind every value, in HQL and native SQL alike
// SECURE - the value cannot alter the query's structure
Query<User> query = session.createQuery(
"FROM User WHERE username = :username", User.class);
query.setParameter("username", username);
User user = query.uniqueResult();
// SECURE - the same rule, and the same syntax, for native SQL
Query<Order> orders = session.createNativeQuery(
"SELECT * FROM orders WHERE user_id = :userId", Order.class);
orders.setParameter("userId", userId);
Why this works: The query text is fixed before any value is supplied. A
bound parameter travels to the database as data - through a prepared statement
in the driver - so no character in it can be read as syntax, whatever quoting or
encoding the attacker tries. Passing the result class to createQuery also
gives a typed Query<User>; the untyped overload carries
@Deprecated(since = "6.0"). A different Hibernate 5.2 deprecation is easy to
conflate with it: that release deprecated the legacy org.hibernate.Query
interface and introduced this typed overload. Nothing is marked forRemoval.
Derived and annotated repository methods
// SECURE - no query string exists to get wrong
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
List<User> findByEmailContaining(String fragment);
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
}
Why this works: Spring Data generates the query from the method signature,
so there is no concatenation step to review. Where an explicit @Query is
needed, :email with @Param binds exactly as setParameter does. Both forms
keep the query in one place, which also makes the remaining hand-written queries
easy to find.
Criteria API for variable filters
// SECURE - conditions are objects, not text
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);
List<Predicate> predicates = new ArrayList<>();
if (username != null) {
predicates.add(cb.equal(user.get("username"), username));
}
if (emailFragment != null) {
predicates.add(cb.like(user.get("email"),
"%" + escapeLike(emailFragment) + "%", '\\'));
}
query.where(cb.and(predicates.toArray(new Predicate[0])));
Why this works: Each condition is a predicate object, and its values are bound when the query is executed, so adding a branch cannot add syntax. This is the pattern for a search form with several optional fields, where string building is most tempting because the concatenation is spread across conditionals.
escapeLike is the application's own helper - there is no such method in JPA or
Hibernate - and it matters for correctness rather than injection, since the
value is bound either way. A % or _ in user input is a wildcard in a LIKE
pattern, so an unescaped search for 100% matches far more than intended and a
lone % scans the table.
Two details decide whether such a helper is right. Escape the escape character
first, before % and _, or the replacements run over each other's output
and the result is double-escaped. And the metacharacter set belongs to the
engine, not to SQL generally: % and _ are universal, but T-SQL also treats
[ as the start of a character class, so on SQL Server a search for [abc]
still behaves as one after escaping only those two. On Spring Data you usually
need none of this - the derived Containing, StartingWith and EndingWith
keywords escape the term for you.
Allowlist identifiers, bind lists
// SECURE - the request selects a key; the application supplies the attribute.
// These are entity attribute paths, not column names: the string is spliced
// into HQL, so `userId` resolves and the underlying `user_id` does not.
private static final Map<String, String> SORT_ATTRIBUTES = Map.of(
"name", "name",
"price", "price",
"category", "category");
String attribute = SORT_ATTRIBUTES.getOrDefault(sortKey, "name");
Query<Product> products = session.createQuery(
"FROM Product ORDER BY " + attribute, Product.class);
// SECURE - the list is bound, not joined
Query<User> users = session.createQuery(
"FROM User WHERE id IN :ids", User.class);
users.setParameterList("ids", validatedIds);
Why this works: The sort key never becomes part of the query text - it selects an entry from a map the application wrote, so an unknown key falls back to a default rather than reaching the parser.
Binding cannot make an identifier dynamic, but the allowlist is not the only
route. Hibernate's own org.hibernate.query.Order resolves a property name
against the entity metamodel: Order.asc(User.class, name) constructs happily,
and applying it raises PathElementException on anything that is not an
attribute, so no hostile string reaches the parser. JPA Criteria's
root.get(name) behaves the same. Prefer the allowlist anyway, because the
metamodel only proves the name is an attribute:
Order.asc(User.class, "password") sorts by the password column, and ordering
is enough to extract it. For the list,
setParameterList (or setParameter with a Collection on a JPA
TypedQuery) expands to the right number of bound parameters, so the values
stay data.
Considerations
- Confirm the concatenated value is actually attacker-influenced. A query
built with
+from an enum, a constant, or a value the application computed is not this weakness, and rewriting it produces churn without reducing risk. Trace the string back to its source before treating the finding as real. - What the query can reach sets the severity. A
SELECTagainst a reference table and a query the application runs as a database owner are the same code defect with very different outcomes. The database user's privileges are what decides whether this is data disclosure or a full compromise, and they are worth checking while fixing - an application user that canDROPis its own finding (CWE-250). - Dynamic ORDER BY is the pattern most likely to survive a cleanup. It
cannot be parameterised, so it needs a different fix from every other case on
this page, and a team that has parameterised its
WHEREclauses often leaves it in place believing the job is done. Check for it specifically. - Know which layer already refuses. Spring Data's
SortandPageableare not the hole they are often assumed to be: a hostile sort property raisesPropertyReferenceExceptionon a derived query, and a@Querymethod - JPQL or native - refuses with "Sort expression must only contain property references or aliases used in the select clause". Hibernate validates through the metamodel in the same way. The API that does pass an expression straight through isJpaSort.unsafe(), which is named for it; so does any sort string you build by hand. Spend the allowlist there rather than onSort.
Testing
A re-scan confirms the concatenation is gone. It cannot confirm the query still returns the right rows, which is where parameterisation most often breaks behaviour.
- Search for
' OR '1'='1and assert the result is zero rows, not every row. A parameterised query treats it as a literal search term that matches nothing, and this single assertion distinguishes a fixed query from a broken one. - Assert an unknown sort key falls back to the default rather than reaching the query, and assert each allowed key actually changes the order. A map that silently returns the default for every key passes the security test and breaks the feature.
- Search for a term containing
%and_and assert it matches literally. AddingLIKEescaping is the change most likely to alter existing search results, and users notice before the tests do. - Assert an empty
INlist behaves as intended.IN ()is a syntax error in most databases, and moving from concatenation tosetParameterListchanges what an empty collection does - decide whether it means "match nothing" and test that decision. - Assert legitimate values containing quotes and apostrophes now work. Names
such as
O'Brienwere often broken by the concatenated version and are the clearest evidence the binding is real.