SQL-инъекции

SQL-инъекция возникает, когда непроверенные данные становятся частью текста SQL-запроса. База не отличает «значение поля» от добавленного пользователем SQL и исполняет всё как одну команду.

Уязвимый код

String login = request.getParameter("login");
String sql = "SELECT id FROM app_user WHERE login = '" + login + "'";
try (Statement statement = connection.createStatement();
     ResultSet result = statement.executeQuery(sql)) {
    // ...
}

Значение ' OR '1'='1 меняет смысл WHERE. Экранирование кавычек вручную не решает задачу надёжно: появляются новые типы данных, кодировки и забытые ветки сборки запроса.

Параметризованный запрос

String sql = """
    SELECT id, login
    FROM app_user
    WHERE login = ? AND status = ?
    """;
 
try (PreparedStatement statement = connection.prepareStatement(sql)) {
    statement.setString(1, login);
    statement.setString(2, status);
    try (ResultSet result = statement.executeQuery()) {
        // чтение результата
    }
}

PreparedStatement передаёт структуру команды и значения раздельно. Введённая строка остаётся значением параметра, а не фрагментом SQL. Тот же принцип используют именованные параметры Spring JDBC и параметры JPQL.

Что нельзя параметризовать

Плейсхолдер заменяет значение, но не имя таблицы, столбца и не направление сортировки. Такие части выбирают из закрытого списка:

String orderBy = switch (sort) {
    case "created" -> "created_at";
    case "name" -> "login";
    default -> "id";
};

После этого допустимо подставить orderBy в текст запроса: значение получено не от пользователя напрямую, а из allowlist.

Дополнительная защита

  • выдать приложению только необходимые права в БД;
  • не показывать клиенту текст SQL и стек драйвера;
  • ограничивать размер и формат входных данных;
  • проверять нативные запросы, динамические фильтры и ORDER BY;
  • тестировать репозитории значениями с кавычками и управляющими символами.

ORM сама по себе не защищает: конкатенация в JPQL или native SQL остаётся уязвимой.

На собеседовании

Почему одной валидации недостаточно? Она зависит от бизнес-правил и может быть обойдена; параметризация устраняет смешение кода и данных на уровне драйвера.

Защищает ли PreparedStatement при конкатенации? Только параметры ?. Если пользовательский фрагмент уже вставлен в SQL-строку, защиты для него нет.

Связанные заметки: JDBC, SQL, Spring Data JPA.