Когда плагину или теме 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 , полезно взять за правило сначала проверять, действительно ли используются заполнители.
Комментарии (0)
Пока нет комментариев — будьте первым.