← Назад к вопросам

Как избежать передачи Injection в JDBC?

2.0 Middle🔥 201 комментариев
#Базы данных и SQL#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ

SQL Injection — это уязвимость безопасности, когда злоумышленник вводит вредоносный SQL код через пользовательский ввод. В JDBC есть несколько способов защиты, и я часто использую их в production коде.

Проблема: SQL Injection

❌ УЯЗВИМЫЙ КОД (Concatenation):

String username = request.getParameter("username"); // Вводит юзер
String email = request.getParameter("email");

// Опасно! Если username = "admin' OR '1'='1
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND email = '" + email + "'";
// Результирующий SQL:
// SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND email = '...'
// Это вернёт всех пользователей!

Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

Строка-вводчик может быть: admin' -- или admin' OR '1'='1' --

Решение 1: PreparedStatement (ГЛАВНЫЙ способ)

✅ БЕЗОПАСНЫЙ КОД:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.sql.DataSource;

public class UserRepository {
    private DataSource dataSource;
    
    public User findByUsernameAndEmail(String username, String email) throws SQLException {
        // PreparedStatement использует параметризованные запросы
        String sql = "SELECT id, username, email, password_hash FROM users " +
                     "WHERE username = ? AND email = ?"; // ? = placeholder
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            
            // Параметры передаются ОТДЕЛЬНО от SQL
            stmt.setString(1, username); // Первый ?
            stmt.setString(2, email);     // Второй ?
            
            // Драйвер JDBC обрабатывает параметры безопасно
            // Специальные символы экранируются автоматически
            try (ResultSet rs = stmt.executeQuery()) {
                if (rs.next()) {
                    return new User(
                        rs.getInt("id"),
                        rs.getString("username"),
                        rs.getString("email")
                    );
                }
            }
        }
        return null;
    }
}

Как это работает:

  1. SQL парсится с placeholders (?)
  2. Драйвер JDBC отправляет SQL-template и параметры отдельно на БД
  3. БД знает, какие части — это SQL, какие — данные
  4. Невозможно "вырваться" из контекста данных в SQL команду

Решение 2: NamedParameterJdbcTemplate (Spring)

С именованными параметрами (более читаемо):

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;

@Repository
public class UserRepositorySpring {
    private NamedParameterJdbcTemplate namedJdbc;
    
    public UserRepositorySpring(NamedParameterJdbcTemplate namedJdbc) {
        this.namedJdbc = namedJdbc;
    }
    
    public User findByUsernameAndEmail(String username, String email) {
        String sql = "SELECT id, username, email FROM users " +
                     "WHERE username = :username AND email = :email";
        
        MapSqlParameterSource params = new MapSqlParameterSource()
            .addValue("username", username)
            .addValue("email", email);
        
        return namedJdbc.queryForObject(sql, params, new UserRowMapper());
    }
}

Решение 3: JPA/Hibernate QueryDSL

С ORM-фреймворком (автоматическая защита):

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UserRepository extends JpaRepository<User, Long> {
    // Автоматически защищено от SQL Injection
    @Query("SELECT u FROM User u WHERE u.username = :username AND u.email = :email")
    User findByUsernameAndEmail(@Param("username") String username,
                                 @Param("email") String email);
    
    // Ещё проще с Query methods
    User findByUsernameAndEmail(String username, String email);
}

Решение 4: Плотные параметры для IN запросов

Проблема с IN (...):

// ❌ УЯЗВИМО
String ids = "1,2,3";
String sql = "SELECT * FROM users WHERE id IN (" + ids + ")";

// ✅ БЕЗОПАСНО
List<Integer> idList = Arrays.asList(1, 2, 3);
String sql = "SELECT * FROM users WHERE id IN (?, ?, ?)";

try (PreparedStatement stmt = conn.prepareStatement(sql)) {
    for (int i = 0; i < idList.size(); i++) {
        stmt.setInt(i + 1, idList.get(i));
    }
    // ...
}

Решение 5: Dynamic SQL с Spring Data (типобезопасно)

import com.querydsl.core.types.dsl.BooleanExpression;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;

public interface UserRepository extends JpaRepository<User, Long>,
                                        QuerydslPredicateExecutor<User> {
}

public class UserService {
    private UserRepository userRepository;
    
    public User findUser(String username, String email) {
        BooleanExpression predicate = QUser.user.username.eq(username)
            .and(QUser.user.email.eq(email));
        
        return userRepository.findOne(predicate).orElse(null);
    }
}

Сравнение способов

СпособЗащитаПримечание
Concatenation❌ НетОпасно, БЕЗ исключений
PreparedStatement✅ ДаСтандартный способ
NamedParameter✅ ДаБолее читаемо
JPA Entities✅ ДаАвтоматическая защита
QueryDSL✅ ДаТипобезопасно
String.format()❌ НетВсё ещё опасно

ВАЖНО: Native Query в JPA

❌ УЯЗВИМО:

@Query(value = "SELECT * FROM users WHERE username = '" + username + "'", 
       nativeQuery = true)
User findByUsername(String username); // ОПАСНО!

✅ БЕЗОПАСНО:

@Query(value = "SELECT * FROM users WHERE username = ?",
       nativeQuery = true)
User findByUsername(String username); // С параметром

Чеклист безопасности

Используй PreparedStatement всегда

String sql = "SELECT * FROM users WHERE id = ?";
stmt.setInt(1, userId);

Используй параметры, а не конкатенацию

// Плохо
"... WHERE name = '" + name + "'"

// Хорошо
"... WHERE name = ?"
stmt.setString(1, name);

Даже с ORM проверяй Native Query

@Query(nativeQuery = true, value = "SELECT * FROM users WHERE id = ?")
User find(Long id);

Валидируй вход на приложении

if (!username.matches("^[a-zA-Z0-9_]+$")) {
    throw new IllegalArgumentException("Invalid username format");
}

Используй минимальные права БД

-- JDBC пользователь может SELECT только, не DROP
GRANT SELECT, INSERT, UPDATE ON tables TO jdbc_user;

Пример из production

@Repository
public class OrderRepository {
    private NamedParameterJdbcTemplate jdbc;
    
    public List<Order> findByOrderNumberAndStatus(String orderNumber, OrderStatus status) {
        String sql = "SELECT * FROM orders " +
                     "WHERE order_number = :orderNumber " +
                     "AND status = :status";
        
        MapSqlParameterSource params = new MapSqlParameterSource()
            .addValue("orderNumber", orderNumber)    // Защищено
            .addValue("status", status.name());      // Защищено
        
        return jdbc.query(sql, params, new OrderRowMapper());
    }
}

Тестирование уязвимостей

@Test
public void testSQLInjectionPrevention() {
    String maliciousInput = "admin' OR '1'='1' --";
    
    // Если используем PreparedStatement, это безопасно
    User result = userRepository.findByUsername(maliciousInput);
    
    // Должен вернуть null или пользователя с ровно таким именем
    assertNull(result); // Если юзер "admin' OR '1'='1' --" не существует
}

Итог

ГЛАВНОЕ ПРАВИЛО:

  • НИКОГДА не конкатенируй пользовательский ввод в SQL
  • ВСЕГДА используй PreparedStatement с параметрами
  • ПРЕДПОЧИТАЙ ORM фреймворки (JPA, QueryDSL) когда возможно
  • ПРОВЕРЯЙ native queries особенно тщательно

SQL Injection — это одна из самых опасных уязвимостей, но она полностью предотвращается простым использованием параметризованных запросов.