Когда плагину или теме WordPress необходимо напрямую обратиться к базе данных, используется глобальный объект  $wpdb , предоставляемый ядром. Чего категорически следует избегать в таком случае, так это подстановки пользовательского ввода напрямую в SQL-строку с помощью конкатенации. Инструмент, который WordPress предлагает взамен — это  $wpdb->prepare() . В этой статье мы рассмотрим, почему это важно и что на самом деле делает эта функция под капотом.

Примечание: SQL-инъекция — это общий термин для атак, при которых непредвиденный ввод смешивается с SQL-выражением, формируемым приложением, изменяя то, что это выражение на самом деле заставляет делать базу данных. Любое значение, поступающее извне приложения (поля форм, параметры URL и т. д.), является отправной точкой, к которой следует относиться с подозрением.

Почему конкатенация строк опасна

Рассмотрим следующий код (чисто иллюстративный псевдокод, а не то, что действительно стоит запускать на рабочем сайте WordPress):

 // Dangerous: user input is dropped straight into the string
$title = $_GET['title'];
$sql = "SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'";
$results = $wpdb->get_results($sql);
 

Это работает нормально до тех пор, пока  $title  является обычной строкой. Но если  $title  задать как-нибудь вроде  ' OR '1'='1 , результирующее SQL-выражение примет следующий вид:

 SELECT * FROM wp_posts WHERE post_title = '' OR '1'='1'
 

Поскольку  '1'='1'  всегда истинно, условие  WHERE  фактически нейтрализуется, и запрос, который должен был возвращать одну конкретную запись, в итоге возвращает их все. Это простой пример, но ту же идею можно применить к получению, изменению или удалению данных, которые приложение никогда не планировало раскрывать.  Первопричиной является сам факт создания SQL-выражения из значения, поступившего извне приложения. Никакая тщательная проверка содержимого значения не устраняет основной риск, пока само выражение собирается путем конкатенации строк.

Что на самом деле решает  $wpdb->prepare() 

Решение, предоставляемое ядром WordPress — это  $wpdb->prepare() , которое используется следующим образом:

 // Safe: use a placeholder instead
$title = $_GET['title'];
$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
    $title
);
$results = $wpdb->get_results($sql);
 

 %s  является заполнителем, и значение, переданное в качестве второго аргумента, вставляется туда. Ключевой момент заключается в том, что  prepare()  не выполняет простую подстановку строк — перед вставкой он  сначала экранирует любые специальные символы в значении, чтобы они никогда не могли быть интерпретированы как часть структуры SQL-выражения. Передайте то же значение  ' OR '1'='1  через  prepare() , и оно будет обрабатываться исключительно как литеральная строка для поиска; структура самого SQL-выражения при этом никогда не изменится.

Почему тип заполнителя имеет значение

 $wpdb->prepare()  поддерживает несколько основных типов заполнителей.

Заполнитель Назначение
 %s  Строка
 %d  Целое число
 %f  Число с плавающей точкой

Причина использовать  %d  или  %f  заключается не только в экранировании — дело в том, что значение  приводится к ожидаемому типу. Для поля ID записи, использующего  %d , такое значение, как  123abc , просто превратится в целое число  123 . С другой стороны, использование  %s  там, где ожидается число, заключает значение в кавычки как строковый литерал в SQL-выражении, что может привести к непредвиденному поведению или ошибкам. Выбор типа заполнителя, соответствующего смыслу передаваемого значения, важен ровно так же, как и само использование  prepare() .

Где обычных заполнителей недостаточно:  LIKE  и  IN 

Условия  LIKE , обычно используемые для функций поиска, являются хорошо известным местом, где базового использования  prepare()  самого по себе недостаточно.  LIKE  интерпретирует  %  и  _  как подстановочные знаки, означающие «любая строка» и «любой одиночный символ» соответственно, поэтому, если поисковый запрос случайно содержит эти символы, результат может оказаться не тем, на который рассчитывал пользователь. Ядро WordPress решает эту проблему с помощью специальной функции  $wpdb->esc_like() , которая экранирует эти подстановочные символы до того, как значение будет передано в  prepare() .

 $keyword = $wpdb->esc_like($_GET['s']);
$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
    '%' . $keyword . '%'
);
 

Аналогичным образом, передача списка значений в предложение  IN (...)  требует динамического создания заполнителя для каждого значения — это нельзя обработать с помощью одного  %s . Стоит иметь в виду, что горстка подобных паттернов не укладывается напрямую в базовый синтаксис заполнителей, о чем всегда полезно помнить при работе с  $wpdb .

Почему эта тема вообще никогда не касается  $wpdb 

Если изучить реализацию темы этого блога,  wpmm-blog , то  $wpdb  прямо не упоминается нигде — ни в  functions.php , ни в каком-либо файле шаблона. Списки записей, архивы и результаты поиска извлекаются через  WP_Query  и теги шаблонов ( have_posts() ,  the_post()  и так далее), поэтому в теме просто нет мест, где сырое SQL-выражение нужно было бы собирать вручную.

Это не случайно — это отражает принцип проектирования, рекомендуемый при разработке тем и плагинов WordPress повсеместно. Высокоуровневые API вроде  WP_Query  выполняют эквивалент  $wpdb->prepare()  под капотом, поэтому вызывающему коду вообще не приходится думать об SQL.  Обращение к высокоуровневому API ядра вместо написания сырого SQL везде, где это возможно — столь же практичное руководство по избежанию SQL-инъекций, сколь и правильное использование  prepare()  там, где сырой SQL действительно необходим.

Итог

Ситуация Что делать
Вставка обычного значения в SQL-выражение Используйте  $wpdb->prepare()  с правильным заполнителем ( %s  /  %d  /  %f )
Значение, используемое внутри предложения  LIKE  Сначала экранируйте подстановочные знаки с помощью  $wpdb->esc_like() , затем передайте результат в  prepare() 
Ситуация, когда сырого SQL можно полностью избежать Позвольте API ядра, такому как  WP_Query , обработать это вместо ручной сборки запроса

Суть защиты от SQL-инъекций заключается в превращении принципа «никогда не доверяй вводу» в конкретную привычку: использование  $wpdb->prepare() . Эту проблему легко упустить из виду, если конкатенация строк вошла в привычку, поэтому всякий раз, когда вы читаете или пишите код, затрагивающий  $wpdb , полезно взять за правило сначала проверять, действительно ли используются заполнители.