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

Быстрые рекомендации.
Правила составления запросов MySQL
Динамическое составление запросов
Правильная работа со спецсимволами при составлении запросов
Подготовленные выражения
SQL Injection
Особенности работы с оператором LIKE
О слешах. Как от них избавиться
Замечания
ОПС: очень полезные ссылки:
Примечание: формы
Комментарии (44)

Быстрые рекомендации.
Для предотвращения SQL инъекций следует соблюдать два простых правила:
1. Не помещать в БД данные без обработки.
Это можно сделать либо с помощью подготовленных выражений, либо обрабатывая параметры вручную.
Если запрос оставляется вручную, то
- все числовые параметры должны быть приведены к нужному типу
- все остальные параметры должны быть обработаны функцией mysql_real_escape_string() и заключены в кавычки.

2. Не помещать в запрос управляющие структуры и идентификаторы, введенные пользователем.
А заранее прописывать в скрипте список возможных вариантов, и выбирать только из них.

Два важных дополнения:
1. Используя изложенную в этой статье информацию, я написал Класс для безопасной и удобной работы с MySQL, который делает запросы безопасными и сокращает код в несколько раз.
2. Если вы не любите сторонние библиотеки, то пользуйтесь хотя бы PDO. Как работать с PDO? Полное руководство.

Правила составления запросов MySQL
Для начала - немного о том, почему вообще нужны эти слеши.
Если мы подставляем в запрос какие-либо данные, то, чтобы отличить эти данные от команд SQL, их надо брать в кавычки.
К примеру, если написать
SELECT * FROM table WHERE name = Bill
то база решит, что Bill - это имя другого поля, не найдёт его, и выдаст ошибку. Поэтому подставляемые данные (в данном случае имя Bill) надо заключать в кавычки - тогда база сочтет его строкой, значение которой надо присвоить полю name:
SELECT * FROM table WHERE name = 'Bill'
Однако, и в самих данных могут встречаться кавычки тоже. К примеру,
SELECT * FROM table WHERE name = 'Д'Артаньян'
Здесь база данных решит, что 'Д' - это данные, а Артаньян - команда, которую она не знает, и тоже выдаст ошибку. Поэтому и надо прослешивать все данные, чтобы объяснить базе, что встречающиеся в них кавычки (и некоторые другие спецсимволы) относятся к данным.
В результате мы получим правильный запрос, который ошибок не вызовет:
SELECT * FROM table WHERE name = 'Д\'Артаньян'

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

Следует специально отметить: добавленные слеши НЕ идут в базу. Они нужны только в запросе. При попадании в базу слеши отбрасываются. Соответственно, распространенной ошибкой является применение stripslashes при получении данных из базы.

Всё вышесказанное относится к данным строкового типа и датам. Числа можно вставлять не прослешивая и не окружaя кавычками. Если вы так делаете, то ОБЯЗАТЕЛЬНО! насильно приводите данные к нужному типу перед вставкой в запрос, например:
$id=intval($id);
Однако для простоты (и надёжности) можно и с числами работать, как со строками (проскольку mysql всё равно преобразует их к нужному типу). Соответственно, мы будем любые данные, вставляемые в запрос, прослешивать и заключать в кавычки.

Так же, есть ещё одно правило - необязательное, но его следует придерживаться во избежание появления ошибок:
Имена полей и таблиц следует заключать в обратные одинарные кавычки - "`" (клавиша с этим символом находится на стандартной клавиатуре слева от клавиши "1") Ведь имя поля может совпадать с ключевыми словами mysql, но если мы используем обратную кавычку, то MySQL поймёт всё правильно:
SELECT * FROM `table` WHERE `date` = '2006-04-04'
Следует различать эти кавычки и не путать одни с другими. Следует также помнить, что обратные кавычки слешами не экранируются.

Динамическое составление запросов
Если SQL запрос в скрипте написан целиком, и никак не меняется, например
SELECT * FROM `table`
то никаких проблем с ним и не будет.
Но вся сила наших скриптов именно в динамическом составлении запросов!
Вместо того, чтобы писать готовые запросы на все случаи жизни, мы составляем их на основании поступающих в скрипт данных.
И вот здесь нас подстерегает опасность.
Допустим, мы составляем запрос с использованием переменной:
SELECT * FROM table WHERE name = '$name'
Вроде бы - всё нормально?
А если $name у нас будет Д'Артаньян? Запрос выдаст ошибку!
То есть, переменную перед подстановкой в запрос надо прослешить.
Это можно сделать несколькими путями.
Самый простой (и неправильный) - положиться на волшебные кавычки. Как вы уже догадались, именно для этого случая они и были придуманы. Ради того, чтобы уберечь SQL запросы забывчивых программистов от ошибок, ВСЕ поступающие в скрипт данные прослешиваются без разбору.
Если вы используете чужой код, то лучше воспользоваться волшебными кавычками. Это может создавать некоторые неудобства и не гарантирует вас от ошибок или взлома (поскольку прослешиванием правила составления запросов не исчерпываются) но хотя бы снижает риск. Поэтому, при использовании чужого кода, обязательно убедитесь, что волшебные кавычки включены.
Если же вы пишете весь код самостоятельно, то следует научиться правильному составлению запросов.

Правильная работа со спецсимволами при составлении запросов
Итак. Как мы уже узнали выше, чтобы правильно составить запрос, надо заключать данные в кавычки и прослешивать их.
С первым всё понятно. При составлении динамических запросов мы никогда не забываем все данные заключить в кавычки:
$query="INSERT INTO `table` VALUES(NULL,'$name','$date','$price')";
Если переменная $price должна быть типа int и мы приведём её к этому типу, то можно её не заключать в кавычки. Однако, если заключим, то беды особой не будет, но зато можно будет сделать работу с данными единообразной.

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

Сначала отключим волшебные кавычки. Так, как это описано в самом начале.
Почему это следует сделать?
По многим причинам. Самая очевидная - логическая. "Волшебные кавычки" добавляют слеши не там, где они нужны - при составлении запроса, а еще до попадания в скрипт! Но ведь данные совсем не обязательно после этого будут вставляться в запрос. Может быть, их придётся выводить пользователю, и слеши будут только мешать. Плюс к тому, добавленные слеши помешают, к примеру, правильно проверить длину введённой строки. К тому же, прослешивать нам надо не только пришедшие от пользователя данные, а вообще любые, вставляемые в запрос - многим этот очевидный факт даже не приходил в голову! Список можно продолжать, но вывод один: добавлять слеши надо не автоматом, без разбору, до начала выполнения скрипта, а только там, где действительно надо – при составлении запроса.
Есть и ещё одна причина: при использовании кодировки Unicode, которая приобретает всё большую популярность, а со временем займёт доминирующее положение в веб, волшебные кавычки могут попросту испортить текст, приняв часть мультибайтной строки за спецсимвол.

Теперь займёмся добавлением слешей самостоятельно.
Во-первых, для прослешивания мы воспользуемся функцией mysql_real_escape_string()
Следует помнить, что применять её можно только после установления соединения с базой.
Эта функция делает гораздо больше, чем устаревшие addslashes и mysql_escape_string. Во-первых, она облегчает ведение и чтение логов mysql, заменяя, например, символ перевода строки на "\n" и некоторые другие символы на escape-последовательности. Во-вторых, и самое главное - она корректно работает с многобайтными кодировками, принимая во внимание текущую кодировку MySQL и не портит, таким образом, тексты в кодировке Unicode.
Во-вторых, не забываем, что прослешить надо все те данные, которые мы заключили в запросе в кавычки:

$name
=mysql_real_escape_string($name);
$age=mysql_real_escape_string($age);
$query="INSERT INTO table (name,age,class) VALUES ('$name','$age',11)";

или:
$query="SELECT * FROM table WHERE name LIKE '".mysql_real_escape_string($_GET['name'])."%'";

Видно, что код получается довольно громоздким. Для облегчения составления запросов можно пуститься на разные ухищнения - сделать функцию для составления запросов из массива (очень удобно для запросов типа INSERT и UPDATE), прослешивать массив данных в цикле, и так далее.
Вы можете написать и свою библиотеку или функцию для составления запросов.
Главное - помнить, что только неукоснительное соблюдение правил составления запросов гарантирует вас от взлома БД, а так же сознавать, что применение "волшебных кавычек", при видимой лёгкости составления запросов, не даёт такой гарантии, а только мешает нормальной работе приложения.

Подготовленные выражения
Есть еще один способ отправлять запросы в БД, называемый "подготовленными выражениями" (prepared statements).
Суть его заключается в том, что подготавливается шаблон запроса, со специальными маркерами, на место которых будут подставлены динамические компоненты. Пример такого шаблона:
SELECT * FROM table WHERE name=?
Знак вопроса здесь - это тот самый маркер. По-другому он называетсй плейсхолдером (placeholder). Весь секрет в том, что данные на его место подставляет специальная функция, которая "привязывает" переменную к запросу.
Вот как выглядит код в таком случае:
$stmt $mysqli->prepare("SELECT District FROM City WHERE Name=?");
$stmt->bind_param("s"$city);
$stmt->execute();

В первой строчке мы подготавливаем шаблон запроса.
Во второй - привязываем к маркеру значение переменной $city.
В третьей строчке выполняем подготовленный таким образом запрос.
При этом запрос и данные идут в базу не вместе, а по отдельности, исключая возможность какой-либо ошибки или злонамеренной манипуляции.

Понятно, что возникает много вопросов. Но ни объём, ни тематика данной статьи не позволяют остановиться на них более подробно. Рекомендую обратиться к документации по библиотекам mysqli и PDO, реализующим данный принцип.
Так же, можно использовать библиотеку DbSimple Дмитрия Котерова или PEAR::DB. Основное отличие этих двух состоит в том, что они реализуют механизм подготовленных выражений только внешне. А внутри работают по-старинке - составляя запрос и отправляя его в базу, беря на себя работу по обработке переменных. А PDO и mysqli работают, как было описано выше - то есть, шаблон запроса и данные уходят в базу по отдельности.

SQL Injection

Итак, мы научились правильно подставлять в запрос данные.
НО! Динамическое составление запросов не исчерпывается подстановкой данных. Часто нам приходится подставлять в запрос команды SQL и имена полей. И здесь мы уже переходим к теме безопасности:

SQL Injection - это способ хакерской атаки, когда передаваемые скрипту данные модифицируются таким образом, что запрос, формируемый в этом скрипте, начинает выполнять совсем не то, для чего он предназначался.
Правила защиты от таких атак можно разделить на два пункта:
1. Работа с данными.
2. Работа с управляющими элементами запроса.

Первый пункт мы подробно рассматривали выше. Он, можно сказать, и не является, собственно, защитой. Соблюдение правил добавления занных в запрос продиктовано, в первую очередь, требованиями СИНТАКСИСА SQL. А как побочный эффект мы имеем и защиту от взлома.

Второй пункт гораздо сложнее, поскольку не существует такого же единого универсального правила, как для данных - обратная кавычка никак не защитит имя поля от модификации хакером. Невозможно кавычками защитить имя таблицы, операторы SQL, параметры команды LIMIT, и другие операторы.
Поэтому основное правило при подстановке управляющих элементов в запрос такое:
Если требуется динамически подставлять в запрос операторы SQL или имена полей, баз данных, таблиц, то ни под каким видом не вставлять их в запрос напрямую.
Все варианты таких добавлений должны быть ЗАРАНЕЕ прописаны в вашем скрипте и выбираться на основании того, что ввёл пользователь.
К примеру, если надо передать имя поля в оператор order by, то ни в коем случае нельзя подставлять его напрямую. Надо сначала проверить его. К примеру, сделать массив допустимых значений, и подставлять в запрос только если переданный параметр в этом массиве присутствует:
$orders=array("name","price","qty");
$key=array_search($_GET['sort'],$orders));
$orderby=$orders[$key];
$query="SELECT * FROM `table` ORDER BY $orderby";

Мы ищем в массиве заранее описанных вариантов введённое пользователем слово, и, если находим, то выбираем соответствующий элемент массива. Если совпадения не будет найдено, то будет выбран первый элемент массива.
Таким образом, в запрос подставляется не то, что ввёл пользователь, а то, что было прописано у нас в скрипте.
Точно так же надо поступать и во всех остальных случаях
К примеру, если динамически формируется оператор WHERE:
if (!empty($_GET['price'])) $where.="price='".mysql_real_escape_string($_GET['price'])."'";
$query="SELECT * FROM `table` WHERE $where";

Мне сложно представить себе случай, когда имя таблицы может подставляться в запрос динамически, но если такое случится, то имя тоже надо вставлять только из заранее прописанного в скрипте набора.
Параметры оператора LIMIT следует принудительно приводить к целочисленному типу с помощью арифметических операций или функции intval().
Не следует думать, что перечисленными здесь примерами исчерпываются все варианты динамического составления запросов. Нужно просто понять принцип, и применять его во всех подобных случаях.

Особенности работы с оператором LIKE
При работе с оператором LIKE существуют некоторые нюансы.

Во-первых, следует обратить внимание на то, что у этого оператора есть два своих спецсимвола - _ и %. Если вы не хотите, чтобы они использовались, как маски, а хотите искать буквальное совпадение с символами % и _, то их надо прослешить. Это можно сделать командой
$data addCslashes($data'%_');
Внимание - это не addslashes! В имени этой функции есть дополнительная буква "c".

Во-вторых, в силу некоторых причин в подставляемых в LIKE (и REGEXP) даных, надо удваивать слеши.

Следовательно, перед тем, как подставлять некую переменную в like, её надо отдельно обработать:
либо прослешить только бэкслеш, если у нас уже стоят символы поиска по маске и мы хотим их использовать по назначению,
$data addCslashes($data'\\');
либо прослешить и символы поиска тоже, если мы хотим их добавить вручную,
$data addCslashes($data'\%_');

В итоге, код подготовки переменной для подстановки в LIKE может выглядеть так:
$data '%'.addCslashes($data'\%_').'%';
и полученное таким образом значение мы уже дальше можем подставлять в запрос, используя либо прослешивание, либо подстановку.

О слешах. Как от них избавиться
Слеш, или бэкслеш, от английского back slash - обратная косая черта ("\"), которая непонятным образом вдруг сама собой появляется в ваших переменных. Добавляется он к некоторым спецсимволам, но в основном его замечают из-за кавычек.
Происходит это из-за специальных настроек PHP, обычно включённых на хостинге по умолчанию. Теоретически, эти настройки могут повысить безопасность скриптов, работающих с БД. Практически же, от автоматического добавления слешей часто получается путаница и неудобство, как при работе с БД, так и при её отсутствии.
Ниже мы подробно разберём оба этих случая.

За автоматическое добавление слешей отвечают директивы php.ini, которые носят общее название "волшебные кавычки":
magic_quotes_gpc и magic_quotes_runtime
Если включена первая, то PHP автоматически добавляет слеши к данным, пришедшим от пользователя - из POST, GET запросов и кук (а так же - к логину и паролю, полученным через HTTP Authorisation).
Если вторая, то слеши добавляются к данным, полученым во время исполнения скрипта - например, из файла или базы данных.

Если вы работаете без базы данных, или же работаете с БД правильно (о чём будет написано ниже), лишние слеши вам только мешают, и от них надо избавляться. Проще и правильнее всего отключить автоматическое добавление, в настройках PHP.
Это можно сделать либо поправив соответствующие директивы в php.ini, если у вас есть к нему доступ, либо создав в коневом каталоге сайта файл .htaccess, и добавив в него строчки
php_flag magic_quotes_gpc 0
php_flag magic_quotes_runtime 0


Если отключить таким образом не получается, то придётся писать код разной степени сложности, чтобы очистить от слешей входящие данные. (Впрочем, если вы хотите написать переносимое приложение, не зависящее от настроек PHP, то написать его всё равно придётся. И включать отдельным блоком в начале ваших скриптов).

С данными, получаемыми во время работы, разобраться проще всего: достаточно в начале скрипта написать
set_magic_quotes_runtime(0);
Для данных, полученных от пользователя, всё гораздо сложнее. Для этого кода нам потребуется две функции:
  • проверить, добавил ли PHP, можно с помощью функции get_magic_quotes_gpc.
  • удаляет слеши функция stripslashes.
    Соответственно, с помощью первой надо проверить, и, если PHP добавил, то перебрать все входящие переменные и очистить с помощью второй.
    Если вы работаете правильно, при register_globals = off, то достаточно применить stripslashes ко всем массивам, содержащим данные, приходящие из браузера.
    к примеру, можно включить во все скрипты сайта вот такой код:
    function strips(&$el) { 
      if (
    is_array($el)) 
        foreach(
    $el as $k=>$v
          
    strips($el[$k]); 
      else 
    $el stripslashes($el); 

    if (
    get_magic_quotes_gpc()) { 
      
    strips($_GET);
      
    strips($_POST);
      
    strips($_COOKIE); 
      
    strips($_REQUEST);
      if (isset(
    $_SERVER['PHP_AUTH_USER'])) strips($_SERVER['PHP_AUTH_USER']); 
      if (isset(
    $_SERVER['PHP_AUTH_PW']))   strips($_SERVER['PHP_AUTH_PW']);
    }

    В случае же неправильных настроек register_globals приемлемое решение и вовсе будет найти затруднительно, поэтому лучше - повторюсь - сразу работать при правильных настройках.

    Замечания
    • Среди причин, по которым не стоит полагаться на "волшебные кавычки", есть ещё одна. Весьма маловероятная, но всё же. К "волшебным кавычкам" относится на самом деле не две директивы, а три. Третья - magic_quotes_sybase. Мало того, что она вместо слеша добавляет кавычку - так она ещё и отменяет действие magic_quotes_gpc. Если каким-то чудом обе эти директивы имеют статус 'on', то последняя не сработает! То есть, полагаясь на "волшебные кавычки", мы в этом случае получим все прелести неправильно составленных запросов. Вообще, чисто теоретически, надо учитывать наличие этой директивы, поскольку она преподносит ещё и такой сюрприз, как... изменение поведения функций addslashes и stripslashes! Если magic_quotes_sybase = on, то эти функции начинают вместо слеша добавлять и удалять одинарную кавычку соответственно.
    • Все приведенные примеры касаются только БД Mysql. Конкретные правила составления запросов могут отличаться для других СУБД, но общий принцип остается прежним:
      • если API для работы с БД или сторонняя библиотека предоставляет специальные функции для составления запросов, и есть возможность их использования, то пользоваться в первую очередь надо ими.

      • если таких функций нет, то следует искать в документации функции экранирования спецсимволов для этой СУБД.


    ОПС: очень полезные ссылки:
    Про волшебные кавычки на сайте PHP.NET
    Про SQL-инъекции на сайте PHP.NET

    Несколько материалов по SQL Injection:
    http://www.securitylab.ru/45438.html
    http://www.securitylab.ru/49424.html
    http://www.nextgenss.com/papers/advanced_sql_injection.pdf

    Примечание: формы
    При выводе value в тегах input форм, слеши не помогают.
    Чтобы текст в таком поле выводился целиком, value надо заключать в кавычки, а к выводимым данным применять функцию htmlspecialchars()
    Пример:
    <input type="text" name="name" value="<? echo htmlspecialchars($name,ENT_QUOTES)?>">
    <textarea><? echo htmlspecialchars($text,ENT_QUOTES)?></textarea>

    Необходимо так же отметить (хоть это уже совсем не имеет отношения к кавычкам и слешам), что функцию htmlspecialchars следует применять при выводе в браузер вообще ко всем данным, которые получены от непроверенного пользователя. Почему это следует делать, можно почитать в гугле по запросу что такое XSS уязвимость