Skip to content

CWE-564: SQL Injection: Hibernate

Overview

Hibernate injection occurs when user input is concatenated into HQL (Hibernate Query Language) or native SQL queries without parameterization, enabling SQL injection attacks despite using an ORM framework, allowing data theft, unauthorized access, and database manipulation.

OWASP Classification

A05:2025 - Injection

Risk

Critical: Hibernate injection enables complete database compromise (data theft, deletion, modification), authentication bypass, privilege escalation, and potential OS command execution through database features, despite ORM abstraction.

Remediation Steps

Core principle: Never build Hibernate/ORM queries by concatenation; use parameter binding and avoid dynamic query fragments from untrusted input.

Locate Hibernate SQL Injection Vulnerabilities

When reviewing security scan results:

  • Find HQL queries: Search for createQuery(), HQL string construction
  • Check for concatenation: Look for + or String.format() building queries
  • Find native SQL: createNativeQuery(), createSQLQuery()
  • Check dynamic ORDER BY: User input in sort clauses
  • Look for IN clause construction: Concatenating lists of IDs

Search patterns:

grep -r "createQuery.*+" --include="*.java"
grep -r "FROM.*WHERE.*+" --include="*.java"
grep -r "createNativeQuery" --include="*.java"
grep -r "ORDER BY.*+" --include="*.java"

Use Parameterized HQL Queries (Primary Defense)

// VULNERABLE - String concatenation in HQL
String username = request.getParameter("user");
String hql = "FROM User WHERE username = '" + username + "'";
Query query = session.createQuery(hql);
List results = query.list();

// Attacker input: user=admin' OR '1'='1
// Resulting HQL: FROM User WHERE username = 'admin' OR '1'='1'
// Returns all users!

// SECURE - Named parameters
String hql = "FROM User WHERE username = :username";
Query query = session.createQuery(hql);
query.setParameter("username", username);  // Safely escaped
List results = query.list();

// Named parameters with multiple values
String hql = "FROM User WHERE username = :username AND role = :role";
Query query = session.createQuery(hql);
query.setParameter("username", username);
query.setParameter("role", role);
List<User> users = query.list();

// Positional parameters
String hql = "FROM User WHERE username = ?1 AND email = ?2";
Query query = session.createQuery(hql);
query.setParameter(1, username);
query.setParameter(2, email);
List<User> users = query.list();

Why this works: Parameterized queries separate SQL structure from data. User input is treated as data, not code. Special characters are automatically escaped.

Use Criteria API or JPA for Type-Safe Queries

// Type-safe Criteria API (recommended)
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);

// Simple equality
cq.select(root).where(
    cb.equal(root.get("username"), username)
);

List<User> users = session.createQuery(cq).getResultList();

// Multiple conditions
List<Predicate> predicates = new ArrayList<>();

if (username != null) {
    predicates.add(cb.equal(root.get("username"), username));
}

if (email != null) {
    predicates.add(cb.like(root.get("email"), "%" + email + "%"));
}

if (active != null) {
    predicates.add(cb.equal(root.get("active"), active));
}

cq.where(predicates.toArray(new Predicate[0]));
List<User> users = session.createQuery(cq).getResultList();

// Spring Data JPA - safest (auto-generated)
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);  // No SQL written!
    List<User> findByEmailContaining(String email);
    List<User> findByUsernameAndActive(String username, Boolean active);

    @Query("SELECT u FROM User u WHERE u.email = :email")
    User findByEmail(@Param("email") String email);
}

Parameterize Native SQL When HQL Insufficient

// VULNERABLE - Native SQL with concatenation
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Query query = session.createNativeQuery(sql, User.class);
List<User> users = query.list();

// SECURE - Parameterized native SQL
String sql = "SELECT * FROM users WHERE username = :username";
Query query = session.createNativeQuery(sql, User.class);
query.setParameter("username", username);
List<User> users = query.list();

// Complex native query with multiple parameters
String sql = "SELECT u.* FROM users u " +
             "JOIN user_roles ur ON u.id = ur.user_id " +
             "WHERE u.username = :username AND ur.role = :role";
Query query = session.createNativeQuery(sql, User.class);
query.setParameter("username", username);
query.setParameter("role", role);
List<User> users = query.list();

Handle Dynamic Queries Safely

// VULNERABLE - Dynamic ORDER BY
String sortColumn = request.getParameter("sort");
String hql = "FROM Product ORDER BY " + sortColumn;
// Attacker: sort=price; DROP TABLE products--

// SECURE - Allowlist for ORDER BY
Set<String> ALLOWED_SORT_COLUMNS = Set.of("name", "price", "category");

String sortColumn = request.getParameter("sort");
if (!ALLOWED_SORT_COLUMNS.contains(sortColumn)) {
    sortColumn = "name";  // Default
}

String hql = "FROM Product ORDER BY " + sortColumn;  // Safe after validation
Query query = session.createQuery(hql);

// Better - use Criteria API for dynamic sorting
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> root = cq.from(Product.class);

if ("price".equals(sortColumn)) {
    cq.orderBy(cb.asc(root.get("price")));
} else if ("name".equals(sortColumn)) {
    cq.orderBy(cb.asc(root.get("name")));
} else {
    cq.orderBy(cb.asc(root.get("id")));
}

// VULNERABLE - IN clause
String idList = request.getParameter("ids");  // "1,2,3"
String hql = "FROM User WHERE id IN (" + idList + ")";
// Attacker: ids=1) OR 1=1--

// SECURE - Parameter list
List<Long> ids = parseIds(idList);  // Validate and convert to Long
String hql = "FROM User WHERE id IN :ids";
Query query = session.createQuery(hql);
query.setParameterList("ids", ids);

Manual Verification Steps

Search for string concatenation in HQL

Find vulnerable queries

grep -r "createQuery.*+" src/
grep -r "\".*FROM.*\" +" src/
# Should return no results - all queries should use parameters

Verify parameterized queries

Check all queries use setParameter

# Count queries
grep -rc "createQuery" src/ | grep -v ":0"

# Count setParameter calls - should be similar or higher
grep -rc "setParameter" src/ | grep -v ":0"

Code review

Manually inspect query construction

// VULNERABLE pattern to find and fix:
String hql = "FROM User WHERE username = '" + userInput + "'";

// SECURE pattern:
String hql = "FROM User WHERE username = :username";
query.setParameter("username", userInput);

Static analysis

Use SAST tools

# SonarQube rule: S2077 (SQL injection)
# Semgrep rule: java.lang.security.audit.sqli.hibernate-sqli
mvn sonar:sonar
semgrep --config=p/sql-injection src/

Security verification checklist

  • No string concatenation in HQL queries (no + operator with user input)
  • All user input passed via setParameter() or setParameterList()
  • Dynamic ORDER BY uses allowlist validation
  • Native SQL is parameterized or avoided
  • Criteria API or Spring Data JPA used where possible
  • SQL injection payloads tested and return 0 or safe results
  • Static analysis reports no HQL injection vulnerabilities

Common Vulnerable Patterns

String Concatenation in HQL Queries

// VULNERABLE - String concatenation in HQL
String hql = "FROM Product WHERE name LIKE '%" + searchTerm + "%'";

Dynamic ORDER BY Without Allowlist

// VULNERABLE - Dynamic ORDER BY
String hql = "FROM User ORDER BY " + sortColumn;
// Attacker: sortColumn=username; DROP TABLE users--

Building IN Clauses with Concatenation

// VULNERABLE - IN clause construction
String hql = "FROM User WHERE id IN (" + idList + ")";
// Attacker: idList=1) OR 1=1--

Native SQL with String Concatenation

// VULNERABLE - Native SQL concatenation
String sql = "SELECT * FROM orders WHERE user_id = " + userId;

Attack Examples

Authentication Bypass via OR Injection

username: admin' OR '1'='1'--
HQL: FROM User WHERE username = 'admin' OR '1'='1'--' AND password = '...'

Data Extraction via UNION Attack

searchTerm: %' UNION SELECT password FROM users WHERE '1'='1

Second-Order Injection from Stored Data

-- Store: username = "admin'--"
-- Later query uses stored value unsafely

Secure Patterns

Use Named Parameters in HQL Queries

// SECURE - Named parameters (best)
@Query("SELECT u FROM User u WHERE u.email = :email")
User findByEmail(@Param("email") String email);

Why this works: Named parameters (:email) separate the HQL query structure from user data. Hibernate treats the parameter value as data only, not executable code. When you call @Param("email"), Hibernate automatically escapes special characters like single quotes ('), ensuring that input like admin' OR '1'='1 is treated as a literal string to match against the email field, not as SQL syntax. This makes SQL injection impossible because the query structure is fixed at compile time - no user input can modify the SELECT or WHERE clauses.

Use Spring Data JPA Query Methods

// SECURE - Spring Data JPA (safe)
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
    List<User> findByEmailContaining(String email);
}

Why this works: Spring Data JPA automatically generates parameterized queries from method names like findByUsername. You never write HQL/SQL directly - Spring creates queries using setParameter() internally, guaranteeing parameterization. Method name parsing happens at application startup, so the query structure is fixed before any user input arrives. Even EmailContaining uses parameterized LIKE clauses. This eliminates concatenation-based injection at the framework level, making it the safest approach for standard CRUD operations.

Use Criteria API for Dynamic Queries

// SECURE - Criteria API with multiple conditions
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 (email != null) {
    predicates.add(cb.like(user.get("email"), "%" + email + "%"));
}

query.where(predicates.toArray(new Predicate[0]));

Why this works: The Criteria API constructs queries using Java objects and method calls (cb.equal(), cb.like()) instead of string concatenation. Each predicate is a type-safe object where values are passed as method parameters - JPA automatically parameterizes these values when generating SQL. Even when building dynamic queries with conditional logic (multiple if statements), you're composing predicate objects, not concatenating strings. The final SQL is generated by JPA with all user values as ? placeholders, preventing injection. This approach combines the safety of parameterization with the flexibility of dynamic query construction.

Additional Resources