W3docs

Java JDBC PreparedStatement

Безопасное выполнение параметризованных SQL-запросов в Java с помощью PreparedStatement для защиты от SQL-инъекций.

PreparedStatement — это SQL-шаблон с заполнителями ?, куда подставляются значения. Значения задаются отдельно по индексу, и драйвер передаёт шаблон и данные по разным каналам — поэтому значение никогда не может быть разобрано как SQL. Это главная привычка в JDBC: она делает инъекцию структурно невозможной и позволяет базе данных переиспользовать план запроса. Предпочитайте PreparedStatement обычному Statement практически везде.

В этой главе рассматривается создание, привязка и выполнение PreparedStatement, почему он останавливает SQL-инъекцию, как привязать NULL и типизированные значения, а также когда выгодно повторно использовать один объект.

Создание, привязка, выполнение

String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, pw);
     PreparedStatement ps = conn.prepareStatement(sql)) {
  ps.setString(1, name);   // bind by 1-based index
  ps.setInt(2, age);
  int rows = ps.executeUpdate();
}

Заполнители нумеруются с 1, а не с 0 — постоянный источник ошибок на единицу. Каждый setXxx соответствует типу столбца: setString, setInt, setBigDecimal, setTimestamp и так далее.

Почему он предотвращает инъекцию

При использовании Statement значение является частью SQL-текста, поэтому кавычка в значении может завершить литерал и внедрить новые команды. С PreparedStatement SQL фиксирован и разбирается до привязки любого значения; значение затем передаётся как типизированный параметр. Нет строки, из которой кавычка злоумышленника могла бы вырваться — опасное значение из главы про Statement просто становится буквальным именем.

После выполнения запроса вы читаете его строки с помощью ResultSet, точно так же, как при работе со Statement.

Привязка NULL и специальных типов

Вы не можете передать Java null в setInt (он принимает примитив), а setString(i, null) неоднозначен для некоторых типов. Явная форма — setNull(index, sqlType), где указывается тип столбца из java.sql.Types:

ps.setNull(3, java.sql.Types.VARCHAR);

Повторное использование подготовленного выражения

Подготовленное выражение создано для многократного выполнения с разными значениями — задать, выполнить, очистить, повторить. База данных разбирает и планирует SQL один раз и переиспользует его, поэтому подготовленные выражения также быстрее в цикле, чем пересборка строки Statement каждый раз. Для массовых вставок сочетайте это с пакетной обработкой.

Практический пример: анатомия шаблона и его привязок

Эта программа рассматривает SQL-шаблон как данные: подсчитывает заполнители, перебирает значения, которые вы бы к ним привязали (включая вредоносную строку, сломавшую Statement), и показывает код типа setNull — все составные части привязки параметров, без реальной базы данных.

java— editable, runs on the server

Что следует усвоить из выполнения:

  • Шаблон содержит три заполнителя ?, и программа их подсчитывает — именно столько вызовов setXxx необходимо сделать. Несоответствие (привязка параметра 4 в запросе с 3 заполнителями) приведёт к исключению при выполнении.
  • Привязки начинаются с 1: параметр 1 — это первый ?. Цикл выводит bind 1, bind 2, bind 3, чтобы закрепить это — начало с 0 является наиболее частой ошибкой новичков.
  • Первое значение — та же строка x'; DROP TABLE users;--, которая взломала Statement в предыдущей главе. Здесь она просто данные, привязанные к параметру 1; драйвер сохраняет её дословно как имя. Инъекция нейтрализована по конструкции, а не экранированием.
  • null в параметре 3 — вот почему существует setNull(index, Types.VARCHAR). JDBC нужен SQL-тип, чтобы сообщить базе данных, какой именно NULL — вы указываете его константой java.sql.Types.
  • Каждое значение несёт неявный тип — String, int, NULL-of-VARCHAR — вот почему существует setXxx для каждого типа, а не один строковый сеттер. Соответствие сеттера типу столбца — это дисциплина, которая делает подготовленные выражения одновременно безопасными и корректными.

Практика

Практика
Почему PreparedStatement предотвращает SQL-инъекцию, даже если привязанное значение содержит одинарную кавычку и точку с запятой?
Почему PreparedStatement предотвращает SQL-инъекцию, даже если привязанное значение содержит одинарную кавычку и точку с запятой?
Was this page helpful?