SQL injection from formatted SQL strings in JPA EntityManager queries

Critical Risk SQL Injection
javajpasql-injectionormhibernate

What it is

SQL injection vulnerability in Java applications using JPA where SQL strings are built with concatenated or formatted variables and executed via EntityManager, bypassing prepared statement protections.

// VULNERABLE: JPA EntityManager with string concatenation
import javax.persistence.*;
import java.util.*;

@Repository
public class ReportRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public List<Object[]> generateCustomReport(String table, List<String> columns,
                                             Map<String, String> filters, String groupBy) {
        // Build dynamic native SQL query
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT ");

        // Add columns
        for (int i = 0; i < columns.size(); i++) {
            if (i > 0) sql.append(", ");
            sql.append(columns.get(i));
        }

        sql.append(" FROM ").append(table);

        // Add filters
        if (!filters.isEmpty()) {
            sql.append(" WHERE ");
            boolean first = true;
            for (Map.Entry<String, String> filter : filters.entrySet()) {
                if (!first) sql.append(" AND ");
                sql.append(filter.getKey()).append(" = '").append(filter.getValue()).append("'");
                first = false;
            }
        }

        // Add grouping
        if (groupBy != null && !groupBy.isEmpty()) {
            sql.append(" GROUP BY ").append(groupBy);
        }

        Query query = entityManager.createNativeQuery(sql.toString());
        return query.getResultList();
    }

    public List<User> searchUsersAdvanced(String nameFilter, String roleFilter,
                                        String sortField, String sortDirection) {
        String jpql = "SELECT u FROM User u WHERE 1=1";

        if (nameFilter != null) {
            jpql += " AND u.name LIKE '%" + nameFilter + "%'";
        }

        if (roleFilter != null) {
            jpql += " AND u.role = '" + roleFilter + "'";
        }

        jpql += " ORDER BY u." + sortField + " " + sortDirection;

        TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
        return query.getResultList();
    }

    public Long getCountByDynamicCondition(String field, String operator, String value) {
        String jpql = "SELECT COUNT(u) FROM User u WHERE u." + field + " " + operator + " '" + value + "'";

        TypedQuery<Long> query = entityManager.createQuery(jpql, Long.class);
        return query.getSingleResult();
    }
}

// Attack examples:
// table = "users; DROP TABLE users; --"
// sortField = "name'; DROP TABLE users; --"
// operator = "= 'admin' OR '1'='1"
// SECURE: Parameterized JPA queries with validation
import javax.persistence.*;
import javax.persistence.criteria.*;
import java.util.*;

@Repository
public class ReportRepository {

    @PersistenceContext
    private EntityManager entityManager;

    // Whitelisted values for security
    private static final Set<String> VALID_TABLES = Set.of("users", "products", "orders");
    private static final Map<String, Set<String>> VALID_COLUMNS = Map.of(
        "users", Set.of("id", "name", "email", "role", "created_date"),
        "products", Set.of("id", "name", "price", "category", "stock"),
        "orders", Set.of("id", "user_id", "total", "status", "order_date")
    );
    private static final Set<String> VALID_SORT_FIELDS = Set.of("name", "email", "role", "createdDate");
    private static final Set<String> VALID_SORT_DIRECTIONS = Set.of("ASC", "DESC");

    public List<Object[]> generateCustomReport(String table, List<String> columns,
                                             Map<String, String> filters, String groupBy) {
        // Validate table name
        if (!VALID_TABLES.contains(table)) {
            throw new IllegalArgumentException("Invalid table name");
        }

        // Validate columns
        Set<String> validColumnsForTable = VALID_COLUMNS.get(table);
        if (!validColumnsForTable.containsAll(columns)) {
            throw new IllegalArgumentException("Invalid column names");
        }

        // Validate groupBy if provided
        if (groupBy != null && !validColumnsForTable.contains(groupBy)) {
            throw new IllegalArgumentException("Invalid group by column");
        }

        // Build secure native SQL with parameters
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT ");

        // Safe to append since columns are whitelisted
        sql.append(String.join(", ", columns));
        sql.append(" FROM ").append(table); // Safe since table is whitelisted

        List<Object> parameters = new ArrayList<>();

        // Add parameterized filters
        if (!filters.isEmpty()) {
            sql.append(" WHERE ");
            boolean first = true;
            for (Map.Entry<String, String> filter : filters.entrySet()) {
                String field = filter.getKey();
                String value = filter.getValue();

                // Validate filter field
                if (!validColumnsForTable.contains(field)) {
                    throw new IllegalArgumentException("Invalid filter field: " + field);
                }

                if (!first) sql.append(" AND ");
                sql.append(field).append(" = ?"); // field is whitelisted, value is parameterized
                parameters.add(value);
                first = false;
            }
        }

        // Add grouping (safe since validated)
        if (groupBy != null && !groupBy.isEmpty()) {
            sql.append(" GROUP BY ").append(groupBy);
        }

        Query query = entityManager.createNativeQuery(sql.toString());

        // Set parameters
        for (int i = 0; i < parameters.size(); i++) {
            query.setParameter(i + 1, parameters.get(i));
        }

        return query.getResultList();
    }

    public List<User> searchUsersAdvanced(String nameFilter, String roleFilter,
                                        String sortField, String sortDirection) {
        // Validate sort parameters
        if (sortField != null && !VALID_SORT_FIELDS.contains(sortField)) {
            sortField = "name"; // default
        }
        if (sortDirection != null && !VALID_SORT_DIRECTIONS.contains(sortDirection.toUpperCase())) {
            sortDirection = "ASC"; // default
        }

        StringBuilder jpql = new StringBuilder("SELECT u FROM User u WHERE 1=1");
        List<Object> parameters = new ArrayList<>();
        int paramIndex = 1;

        if (nameFilter != null && !nameFilter.trim().isEmpty()) {
            jpql.append(" AND u.name LIKE ?").append(paramIndex);
            parameters.add("%" + nameFilter + "%");
            paramIndex++;
        }

        if (roleFilter != null && !roleFilter.trim().isEmpty()) {
            jpql.append(" AND u.role = ?").append(paramIndex);
            parameters.add(roleFilter);
            paramIndex++;
        }

        // Safe to append since validated
        if (sortField != null) {
            jpql.append(" ORDER BY u.").append(sortField).append(" ").append(sortDirection);
        }

        TypedQuery<User> query = entityManager.createQuery(jpql.toString(), User.class);

        // Set parameters
        for (int i = 0; i < parameters.size(); i++) {
            query.setParameter(i + 1, parameters.get(i));
        }

        return query.getResultList();
    }

    // Better approach: Use Criteria API for complex dynamic queries
    public List<User> searchUsersCriteria(String nameFilter, String roleFilter,
                                        String sortField, String sortDirection) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);

        List<Predicate> predicates = new ArrayList<>();

        if (nameFilter != null && !nameFilter.trim().isEmpty()) {
            predicates.add(cb.like(user.get("name"), "%" + nameFilter + "%"));
        }

        if (roleFilter != null && !roleFilter.trim().isEmpty()) {
            predicates.add(cb.equal(user.get("role"), roleFilter));
        }

        if (!predicates.isEmpty()) {
            query.where(cb.and(predicates.toArray(new Predicate[0])));
        }

        // Add sorting with validation
        if (VALID_SORT_FIELDS.contains(sortField)) {
            if ("DESC".equalsIgnoreCase(sortDirection)) {
                query.orderBy(cb.desc(user.get(sortField)));
            } else {
                query.orderBy(cb.asc(user.get(sortField)));
            }
        }

        return entityManager.createQuery(query).getResultList();
    }
}

💡 Why This Fix Works

The vulnerable code directly concatenates user input into JPA queries, allowing injection attacks. The fixed version uses parameterized queries with setParameter(), validates all inputs against whitelists, and demonstrates both native SQL and Criteria API approaches for secure dynamic queries.

Why it happens

Using string concatenation or formatting to build native SQL queries in JPA EntityManager, allowing injection through user-controlled values.

Root causes

String Concatenation in JPA Native Queries

Using string concatenation or formatting to build native SQL queries in JPA EntityManager, allowing injection through user-controlled values.

Preview example – JAVA
// VULNERABLE: String concatenation in JPA native queries
@Service
public class UserService {

    @PersistenceContext
    private EntityManager entityManager;

    public List<User> searchUsers(String name, String department, String sortBy) {
        String query = "SELECT u FROM User u WHERE u.name LIKE '%" + name + "%'";

        if (department != null) {
            query += " AND u.department = '" + department + "'";
        }

        query += " ORDER BY u." + sortBy;

        return entityManager.createQuery(query, User.class).getResultList();
    }
}

Dynamic JPQL Construction with User Input

Building dynamic JPQL queries by concatenating user input directly into query strings without parameterization.

Preview example – JAVA
// VULNERABLE: Dynamic JPQL with user input
public class ProductRepository {

    @PersistenceContext
    private EntityManager em;

    public List<Product> findWithFilters(Map<String, String> filters, String sortField) {
        StringBuilder jpql = new StringBuilder("SELECT p FROM Product p WHERE 1=1");

        for (Map.Entry<String, String> filter : filters.entrySet()) {
            jpql.append(" AND p.").append(filter.getKey())
                .append(" = '").append(filter.getValue()).append("'");
        }

        jpql.append(" ORDER BY p.").append(sortField);

        Query query = em.createQuery(jpql.toString());
        return query.getResultList();
    }
}

Fixes

1

Use Named Parameters in JPQL

Replace string concatenation with named parameters in JPQL queries. Use setParameter() to bind values safely.

View implementation – JAVA
// SECURE: Named parameters in JPQL
@Service
public class UserService {

    @PersistenceContext
    private EntityManager entityManager;

    private static final Set<String> VALID_SORT_FIELDS = Set.of(
        "name", "email", "department", "createdDate"
    );

    public List<User> searchUsers(String name, String department, String sortBy) {
        // Validate sort field against whitelist
        if (!VALID_SORT_FIELDS.contains(sortBy)) {
            sortBy = "name";
        }

        StringBuilder jpql = new StringBuilder("SELECT u FROM User u WHERE u.name LIKE :name");

        if (department != null && !department.trim().isEmpty()) {
            jpql.append(" AND u.department = :department");
        }

        // Safe to append since sortBy is validated
        jpql.append(" ORDER BY u.").append(sortBy);

        TypedQuery<User> query = entityManager.createQuery(jpql.toString(), User.class);
        query.setParameter("name", "%" + name + "%");

        if (department != null && !department.trim().isEmpty()) {
            query.setParameter("department", department);
        }

        return query.getResultList();
    }
}
2

Use Criteria API for Dynamic Queries

For complex dynamic queries, use JPA Criteria API which provides type-safe query construction and automatic parameterization.

View implementation – JAVA
// SECURE: JPA Criteria API for dynamic queries
import javax.persistence.criteria.*;

@Service
public class ProductService {

    @PersistenceContext
    private EntityManager em;

    private static final Set<String> VALID_FILTER_FIELDS = Set.of(
        "name", "category", "brand", "status"
    );

    private static final Set<String> VALID_SORT_FIELDS = Set.of(
        "name", "price", "createdDate", "rating"
    );

    public List<Product> findWithFilters(Map<String, String> filters, String sortField) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Product> query = cb.createQuery(Product.class);
        Root<Product> product = query.from(Product.class);

        // Build predicates for filters
        List<Predicate> predicates = new ArrayList<>();

        for (Map.Entry<String, String> filter : filters.entrySet()) {
            String field = filter.getKey();
            String value = filter.getValue();

            // Validate field against whitelist
            if (VALID_FILTER_FIELDS.contains(field) && value != null && !value.trim().isEmpty()) {
                predicates.add(cb.equal(product.get(field), value));
            }
        }

        // Apply predicates
        if (!predicates.isEmpty()) {
            query.where(cb.and(predicates.toArray(new Predicate[0])));
        }

        // Add sorting with validation
        if (VALID_SORT_FIELDS.contains(sortField)) {
            query.orderBy(cb.asc(product.get(sortField)));
        } else {
            query.orderBy(cb.asc(product.get("name"))); // default sort
        }

        return em.createQuery(query).getResultList();
    }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies sql injection from formatted sql strings in jpa entitymanager queries and many other security issues in your codebase.