Как избежать передачи Injection в JDBC?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ
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;
}
}
Как это работает:
- SQL парсится с placeholders (?)
- Драйвер JDBC отправляет SQL-template и параметры отдельно на БД
- БД знает, какие части — это SQL, какие — данные
- Невозможно "вырваться" из контекста данных в 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 — это одна из самых опасных уязвимостей, но она полностью предотвращается простым использованием параметризованных запросов.