Php mysql html escape

Защита от SQL-инъекций в PHP

SQL инъекция — это подстановка в SQL-запрос таких данных, которые меняют структуру этого запроса. Злоумышленник может использовать уязвимость для выполнения произвольного SQL-кода.

Представим типичную задачу — вывод статей на сайте. При переходе по адресу /index.php?id=15 должна быть отображена статья, идентификатор которой в базе данных равен числу 15.

Как начинающие разработчики обычно пишут запрос к базе данных:

$query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id'];

Разработчик ожидает, что в $_GET[‘id’] будет число и конечный запрос станет таким:

SELECT * FROM `articles` WHERE `id` = 15

Но вместо этого злоумышленник может передать строку -1 OR 1=1 :

SELECT * FROM `articles` WHERE `id` = -1 OR 1=1

При запуске этого запроса будут выбраны все записи вместо одной, поскольку записей с отрицательными идентификаторами скорее всего нет в базе, а условие 1=1 всегда истинно.

Но суть в другом. После фрагмента 1=1 злоумышленник может дополнить запрос любым произвольным SQL-кодом.

Что может сделать злоумышленник?

Это зависит от конкретного запроса, а также способа его запуска.

Если запрос выполняется не через функцию mysqli_multi_query() , которая поддерживает мультизапросы (несколько запросов через точку с запятой), тогда у злоумышленника нет возможности выполнить совсем произвольный запрос вроде такого:

SELECT * FROM `articles` WHERE `id` = 1; DROP TABLE `articles`

Так сделать не получится, поскольку выполнение нескольких запросов по-умолчанию не поддерживается.

Но кое-что плохое злоумышленник сделать может. Например, с помощью UNION можно получить любые данные из любых таблиц.

Представим, что у нас есть таблица articles с 4 полями: id | title | content | created_at , а также таблица users с 3 полями: id | login | password .

Поскольку UNION позволяет объединять данные из таблиц только с одинаковым количеством столбцов, злоумышленник может указать 2 необходимых ему столбца, а остальные 2 заполнить любыми значениями, например единицами:

SELECT * FROM `articles` WHERE `id` = -1 UNION SELECT 1, `login`, `password`, 1 FROM `users`

В итоге вместо title и content на страницу будут выведены login и password одного из пользователей. И это только один из десятков возможных вариантов взлома.

Экранирование кавычек

Прежде чем перейти к существующим способам защиты, хочу отдельно объяснить, что такое вообще экранирование и зачем оно нужно.

$name = 'Вася'; $query = "UPDATE `users` SET `name` = '$name'";

С этим запросом всё в порядке, он выполнится как мы и ожидаем:

UPDATE `users` SET `name` = 'Вася'

Но что если в переменной $name будет одинарная кавычка?

Тогда SQL-запрос станет таким:

UPDATE `users` SET `name` = 'Д'Артаньян'

Попытка выполнить этот запрос приведёт к ошибке синтаксиса. Чтобы её не было, вторую кавычку нужно экранировать, т.е. добавить к ней обратный слеш.

Способы экранирования и их надёжность разберём чуть ниже, а сейчас для простоты возьмём addslashes() :

UPDATE `users` SET `name` = 'Д\'Артаньян'

Готово, запрос выполнится даже при наличии кавычек.

Экранировать можно не только кавычки. Разные функции умеют экранировать разные символы, об этом мы подробно поговорим чуть позже.

А теперь важный момент. Некоторые разработчики считают, экранирования достаточно для полной защиты от SQL-инъекций.

Хорошо, ещё раз посмотрим на самый первый пример с SQL-инъекцией:

$_GET['id'] = '-1 OR 1=1'; $query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id'];
SELECT * FROM articles WHERE OR 1=1

В этом запросе нет никаких кавычек. Но уязвимость есть. Отсюда делаем вывод, что экранирование не гарантирует защиту от SQL-инъекций.

Неэффективные способы защиты от SQL-инъекций

Очевидно, самый худший вариант — не иметь никакой защиты от SQL инъекций и передавать данные, полученные от пользователя, напрямую в SQL-запрос.

$query = 'SELECT * FROM `users` WHERE `id` = ' . $_GET['id'];

Никогда так не делай! Любые данные перед подстановкой в SQL-запрос должны проходить фильтрацию и/или валидацию.

1. Функция htmlspecialchars()

Время от времени встречаю статьи, где авторы используют функцию htmlspecialchars() для экранирования данных:

$name = "Д'Артаньян"; $name = htmlspecialchars($name); $query = "UPDATE `users` SET `name` = '$name'";

Это опасно! Штука в том, что функция htmlspecialchars() пропускает без экранирования опасные символы: \ (слеш), \0 (nul-байт) и \b (backspace).

Вот полный пример кода, демонстрирующего уязвимость:

$mysqli = new mysqli('localhost', 'root', 'password', 'database'); $login = '\\'; $password = ' OR 1=1 #'; $login = htmlspecialchars($login, ENT_QUOTES, 'UTF-8'); $password = htmlspecialchars($password, ENT_QUOTES, 'UTF-8'); $sql = "SELECT * FROM `users` WHERE `login` = '$login' AND `password` = '$password'"; $items = $mysqli->query($sql) or die($mysqli->error); while($item = $items->fetch_assoc()) < var_dump($item); echo '
'; >

В итоге SQL-запрос будет таким:

SELECT * FROM `users` WHERE `login` = '\' AND `password` = ' OR 1=1 #'

С помощью / экранируется кавычка, идущая сразу после $login. `login` = ‘$login’ по факту превращается в `login` = ‘\’ AND `password` = ‘ . После этого любой код, который мы напишем, будет выполнен, в нашем случае это просто OR 1=1 . В конце добавляем # (комментарий), чтобы скрыть последнюю кавычку.

2. Фильтрация по чёрному списку символов

По каким-то непонятным мне причинам ещё существуют разработчики, использующие чёрные списки символов:

$disallow = ['~', '\'', '"', '', '.', '%']; $name = 'Вася'; $name = str_replace($disallow, '', $name); $query = "SELECT * FROM `users` WHERE `name` = '$name'";

Все символы, входящие в чёрный список, удаляются из строки перед вставкой в базу.

Я не хочу сказать, что этот подход не будет работать, но его применение под большим вопросом:

  • Зачем вообще составлять какие-то списки, если есть более простые и надёжные способы защиты?
  • Нужно знать все потенциально опасные символы.
  • Что делать если нужно разрешить пользователям использовать какие-либо символы из списка?

Кроме этого, я считаю фильтрацию в SQL-запросах плохой идеей. Если в строке есть недопустимые символы — лучше сообщить о них пользователю и попросить исправить, а не просто обрезать часть контента.

К примеру, пользователь хочет использовать логин ~!Mega_!Pihar!_!9000!~ , а после регистрации оказывается, что его ник превратился в MegaPihar9000 .

Я считаю, лучше уточнить у пользователя, нравится ли ему такой отфильтрованный логин или он хотел бы что-то поменять. Короче, я за валидацию по белому списку вместо фильтрации по чёрному.

3. Функция stripslashes()

Редко, но встречается код, использующий stripslashes() перед записью в базу. Поскольку новички до сих пор копируют этот код в свои проекты, объясню, зачем эта функция нужна.

Раньше в PHP была такая штука как волшебные кавычки (Документация). Если эта директива была включена, то все данные, содержащиеся в $_GET, $_POST и $_COOKIE автоматически экранировались.

Сделано это было для защиты новичков, которые подставляли данные напрямую в SQL-запросы. На практике это было не самое удачное решение:

  • Не очень удобно, когда все данные по-умолчанию экранируются, ведь зачастую они нужны в исходном виде.
  • В идеале экранирование должно учитывать кодировку соединения с базой данных, о чём мы поговорим чуть позже. Из-за этого разработчикам приходилось убирать экранирование функцией stripslashes() и затем опять экранировать данные более подходящими функциями, в случае MySQL это была mysql_real_escape_string() .

Вот почему функцию stripslashes() можно встретить в старых учебниках. Чтобы отменить экранирование символов и получить исходную строку.

Начиная с PHP 5.4 функционал волшебных кавычек удалён, поэтому использовать stripslashes() перед записью в базу нет никакого смысла.

4. Функция addslashes()

В некоторых книгах ещё можно встретить рекомендации экранировать данные функцией addslashes() .

Эта функция надёжней, чем htmlspecialchars() , поскольку экранирует и обратный слеш, и nul-байт. Однако эта функция хуже, чем mysql_real_escape_string , поскольку не учитывает кодировку текущего соединения с базой.

Поэтому даже в документации прямо написано, что эту функцию не нужно использовать для защиты от SQL инъекций.

Вырезка из документации по поводу уязвимости функции addslashes()

Эффективные способы защиты

1. Функция mysql(i)_real_escape_string

Работает эта функция примерно по тому же принципу, что и addslashes() , только учитывает текущую кодировку соединения с базой данных.

Есть две важные детали, которые вы должны знать, когда используете эту функцию.

Первая — вы всегда должны подставлять экранированные данные в кавычки. Если этого не делать, толку от экранирования не будет:

// Неправильно, сначала надо экранировать! $query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id']; // Экранируем $id = $mysqli->real_escape_string($_GET['id']); // Тоже неправильно, нет кавычек $query = 'SELECT * FROM `articles` WHERE `id` = ' . $id; // Правильно $query = "SELECT * FROM `articles` WHERE `id` = '$id'";

Вторая опасность подстерегает тех, кто использует некоторые специфические кодировки вроде GBK. В этом случае вам обязательно нужно указывать кодировку при установке соединения с базой.

Почитать о проблеме можно тут (блог разработчика, обнаружившего ошибку), здесь и подробней с примерами там.

2. Приведение к числу

Простой и эффективный способ защиты для числовых полей — приведение данных к числу. Пример:

$_POST['id'] = '15'; $id = (int) $_POST['id']; // Или так: $id = intval($_POST['id']); // Или для дробных чисел: $id = (float) $_POST['id']; $query = 'SELECT * FROM `users` WHERE `name` = ' . $id;

Кавычки здесь не обязательны, поскольку в запрос в любом случае подставится число.

Есть один нюанс. Как я писал выше, мне не очень нравится идея фильтрации данных и здесь она может выйти боком с точки зрения SEO.

Допустим, есть интернет-магазин, где URL адреса страниц товаров выглядят как /product/15 , где 15 — идентификатор товара.

Если алгоритм поиска статьи заключается в том, что мы берём вторую часть URL и приводим её к числу, вроде такого:

$segments[2] = '15'; $id = (int) $segments[2];

Тогда можно писать какие угодно символы после числа 15 (только один следующий символ должен быть не цифровым), например /product/15abcde13824_ahaha_lol и система всё равно будет отображать статью c >

3. Подготовленные запросы

Один из лучших способов защиты от SQL инъекций. Суть в том, что SQL запрос сначала «подготавливается», а затем в него отдельно передаются данные.

$stmt = $db->prepare('SELECT * FROM `users` WHERE `name` LIKE ?'); $stmt->execute([$_GET['name']]);

Такой подход гарантирует отсутствие SQL-инъекций в момент подстановки данных, поскольку запрос уже «подготовлен» и не может быть изменён.

Но, как обычно, всё портят детали.

Первая деталь. Чуть выше я указывал ссылку на обсуждение уязвимости mysql_real_escape_string.

Если ты героически прочитал его до конца (нет), там есть интересное утверждение — что PDO с подготовленными запросами также может иметь уязвимость, связанную с кодировками.

Чтобы её избежать, нужно либо отключить эмуляцию подготовленных запросов, либо использовать только надёжные кодировки (например UTF-8), либо обязательно указывать кодировку соединения (через $mysqli->set_charset($charset) или DSN для PDO, но не через SQL-запрос SET NAMES).

Вторая деталь. Нужно понимать, что защита от SQL-инъекций будет действовать только в том случае, если мы не подставляем никаких данных напрямую в запрос. Если разработчик решит сделать так:

$stmt = $db->prepare("SELECT * FROM `users` WHERE `name` = '$_POST[name]'"); $stmt->execute();

Тогда его не спасут никакие подготовленные запросы.

И третья деталь. В подготовленные запросы нельзя подставлять названия столбцов и таблиц.

// Так делать нельзя $stmt = $pdo->prepare('SELECT ? FROM ?);

Прекрасно. И что теперь делать?

Один из распространённых вариантов — белые списки. Простой пример:

$_POST['product'] = [ 'title' => 'Название товара', 'article' => 'Артикул товара', 'content' => 'Описание товара' ]; $allowed = ['title', 'article', 'content']; foreach($_POST['product'] as $k => $v)

Если полей много и не хочется всех их вбивать ручками — можно просто достать их всех из базы ( SHOW COLUMNS FROM `products`) .

Другой логичный вариант — валидировать названия столбцов, разрешая, к примеру, только буквы, цифры и подчёркивания.

В общем, опять надо что-то вручную допиливать, придумывать собственные функции генерации запросов. Не комильфо. Рекомендую поступить иначе.

4. Готовые библиотеки

Разработчики популярных библиотек наверняка гораздо умней и опытней нас. Они давно всё продумали и протестировали на десятках тысяч программистов. Так почему нет?

Для простых проектов вполне хватит Medoo или RedBeanPHP, для средних рекомендую (и всегда использую) Eloquent, ну а для крупных проектов лучше всего подойдёт мощная и суровая Doctrine.

Источник

Читайте также:  Python подготовка для егэ
Оцените статью