Black#FFFFFF
Постоялец
- Регистрация
- 19 Июл 2007
- Сообщения
- 228
- Реакции
- 172
- Автор темы
- #1
Написание безопасных web-приложений.
Содержание:
Аннотация или суть проблеммы:
Приходится писать много кода. Иметь дело с огромным количеством вэб приложений. К сожалению, раз за разом среди совсем различных скриптов встречаю одни и те же ошибки, которые приводят к большим проблемам безопасности в среде разрабатываемого вэб приложения. Это и типичные ошибки, связанные с отсутствием или недостаточной фильтрацией данных, передаваемых от пользователя на сервер, и предоставление отладочной информации, информации об ошибках приложения и предупреждениях прямо в браузер на клиентской вэб странице, и недостаточная проверка содержимого файлов, загружаемых через POST.
Целью этой статьи будет вкратце ознакомить Вас с основными видами угроз безопасности web приложений и привести пример построения защиты стандартными средствами функций обработки данных и настройки выполнения скриптов в среде приложения, использующей для своего функционирования PHP 4+ Mysql 3.2+.
В основном благодаря различного рода уязвимостям и предоставлению отладочной информации на страницах поддерживаемого или разрабатываемого Вами вэб приложения удаленный пользователь, выполнив определенные действия, после поиска уязвимостей, сможет получить доступ практически к любой информации, циркулирующей внутри вашего скрипта, речь здесь ведется не только о нанесении урона конфиденциальности информации, как например потери данных о кредитных карточках, если мы имеем дело с интернет магазином, но и о возможности полного уничтожения вэб приложения и результатов его работы.
Согласитесь, не слишком радужная перспектива?
Примечание:
Статья от Вас потребует определенного уровня знаний. Вы должны быть как минимум поверхностно знакомы с базами данных и синтаксисом запросов SQL, иметь представление о работе с функциями в языке PHP, и о разметке HTML страниц, а так же о способе передачи информации от клиента к серверу.
Начнем с обзора основных уязвимостей:
Итак, какие виды уязвимостей бывают, об этом было довольно много сказано.
Рассмотрим три основные из них: Сode injection (php including), sql injection, xss scripting. (Краткая информация с описанием инъекций размещенная и дополненная здесь, взята со страниц журнала Для просмотра ссылки Войдиили Зарегистрируйся Внутримышечно и внутривенно, Спецвыпуск: Хакер, номер #075, стр. 030, Обзор технологий взлома веб-ресурсов).
Сode injection
В теории все выглядит просто: есть скрипт, исполняемый на сервере, в который взломщику необходимо встроить свой код. Провернуть такую махинацию довольно просто, если соблюдены два условия. Во-первых, веб-разработчик должен использовать конструкцию include с параметром переменной, а во-вторых, должен плохо проверять данные, поступающие от пользователя. Наиболее часто такая ситуация возникает в простых скриптах:
Язык: HTML + PHP
Соответственно, работа идет со ссылками типа Для просмотра ссылки Войдиили Зарегистрируйся. Самое безобидное, что можно сделать – это просмотреть информацию о PHP (и не только
Листинг
Язык: PHP
Создав такой файл у себя на сервере, просто включаем его в запрос вместо about.php – и видим всю информацию на экране. Таким образом, мы внедрили произвольный код в скрипт на сервере и получили необходимую информацию. Но это только цветочки, ведь можно при помощи PHP сделать все, что нашей душе угодно.
SQL Injection
Давным-давно, когда по земле еще ходили динозавры, веб-программеры использовали для хранения данных текстовые файлы. Потом, когда человек изобрел колесо, веб-разработчики придумали базы данных и стали хранить все в них. И было всем счастье – и список пользователей туда засунуть можно, и все документы на сайте положить. Но однажды один умелец случайно ввел в форму апостроф, и выдал скрипт SQL-ошибку. Прочитал умелец сообщение, подумал немного и ввел вместо апострофа
Листинг
• Язык: SQL
еще немного поколдовал и стал с тех пор администратором. Работа с базой ведется на языке SQL, например, чтобы проверить, что пользователь существует, можно сделать такой запрос по логину:
Листинг
• Язык: SQL
А теперь подставь в запрос апостроф или ' OR '1'='1’ и посмотри, что получится. Фактически, мы можем выполнить произвольный SQL-запрос, и случиться что-нибудь нехорошее:
Листинг
• Язык: SQL
Хочу кинуть еще пару хороших идей, как можно воспользоваться SQL-инъекцией. Для этого мы рассмотрим детали диалектов языка SQL у разных производителей. Начнем с MySQL, который делает то, что я от него ни как не ожидал . А проблема проста: если SELECT-запрос подвержен инъекции, то не факт, что его результат будет выведен на экран, но если внимательно почитать руководство по MySQL, то можно найти замечательный функционал – результат запроса можно перенаправить в файл!
Листинг
• Язык: SQL
Теперь осталось найти каталог, который нас приютит. В случае с CMS все решается довольно просто – почти всегда есть каталог для загрузки файлов upload. Именно в такой каталог и стоит перенаправлять вывод.
Начиная с четвертой версии, MySQL поддерживает объединение запросов при помощи команды UNION. Таким образом, можно вывести дополнительную информацию из произвольной таблицы. Посмотрим, как это выглядит на примере:
Листинг
• Язык: SQL
Title и description имеют тип varchar, поэтому переменной $id нужно присвоить такое значение, чтобы при подстановке получился следующий запрос:
Листинг
• Язык: SQL
Обрати внимание, как я использовал комментарий – при Union-инъекциях – это стандартная практика отсечения ненужной части строки.
Примечание:
На самом деле здесь требуется выполнение еще одного условия, чтобы подобрать нужный тип запроса для SQL инъекций, необходимо чтобы разработчик использовал вывод сообщения об ошибках в стандартный поток браузера. Что практикуется очень часто.
Пример:
Примерно вот так разработчик «выводит» возникшую при запросе к базе данных ошибку в браузер, что, зачастую, помогает злоумышленнику, предоставляя необходимую информацию для составления запроса с SQL injection.
XSS
В наше время на большинстве сайтов пользователи имеют возможность создавать свои материалы и комментировать чужие. Для красивого оформления пользователю дается возможность вводить данные в формате HTML. Ввод HTML-кода может быть разрешен напрямую, либо при помощи WYSIWYG-редактора. Казалось бы, все довольны! Особенно взломщики . Есть хорошая пословица: «Где HTML, там и JavaScript». Таким образом, при вводе можно использовать тег <script> для исполнения произвольного JavaScript (и не только его, кстати). А если мы можем использовать скрипты, которые исполняются на стороне пользователя, значит, мы можем украсть его куки! А тут уже и до взлома аккаунта недалеко…
Примечание:
Здесь не все так просто, получить можно не только cookie клиента, но можно изменить поведение клиентской страницы буквально произвольно, по желанию, от нарушения дизайна сайта, дефейса уязвимого сайта, вплоть до полного перенаправления всей информации с сайта источника на другой сайт. И дело не только в javascript, например без использования javascript вполне возможно добавить <iframe>, <frameset> и многие другие не желательные теги, включить flash тэги <embeded>, <object> и апллеты в html в содержимое страницы.
Фрагментированные XSS-атаки
Часто бывает так, что информация, вводимая пользователем, хорошо фильтруется, и к форме на кривой козе не подступишься. Но если в ней несколько полей, например, «название материалов» и «содержание материалов», то можно попробовать более сложный вид атаки – фрагментированный. При таком подходе, необходимо, чтобы данные из разных полей формы выводились последовательно, и тогда появляется вероятность того, что вместе «безобидные строчки» станут грозным оружием.
Советы по проектированию безопасного web приложения.
Избавляемся от Code injection:
Как обезопаситься? Здесь все просто. Передавайте через GET,POST массивы скрипты, которые нужно подключить в данном приложении, не напрямую, а косвенно, через определенную переменную, и, на основании ее значения, подключайте тот или иной скрипт, используя include или require.
Пример:
Нам нужно подключить содержимое файла example.php в index.php.
Передаем в GET переменную file значение 1.
Index.php?file=1
Код файла index.php:
Замечание:
Иногда угроза CODE injection возникает еще по одной причине. Некоторые начинающие вэб разработчики подключение самых обыкновенных файлов используют с помощью include,require, либо стараются придать конфигурационным файлам отличное от php расширение, .ini и прочее.
Предостерегаю вас от подобных ошибок. Для функции работы с файлами, пожалуйста, используйте набор стандартных функций для получения информации из файлов (file,file_get_contents,fread,fgetc,fpassthru,fgetss). Но не в коем случае не выполняйте их содержимое, как код PHP. Для конфигурационных файлов пожалуйста сохраняйте их с расширением .php. Web сервер сам поможет Вам скрыть содержимое данных файлов от просмотра через web браузер.
Избавляемся от SQL injection.
Первая проблема: Вывод информации об ошибках базы данных в браузер пользователя.
Нам нужно определиться, когда этап отладки миновал, стоит ли нам информацию об ошибках SQL, возникшую при работе скриптов, выводить в браузере клиента? По-моему нет. Но иногда требуется все же иметь доступ к этой информации, которая во многом в процессе работы приложения помогает устранять ошибки и недостатки. Как же быть?
Казалось бы, выход очевиден.
Используем попытку запрета вывода об ошибках вообще:
Но, ошибки базы данных, выводимые в браузер разработчиками с помощью
всеже останутся.
Не следует действительно писать:
PHP предоставляет для этих случаев функцию trigger_error.
bool trigger_error ( string $error_msg [, int $error_type= E_USER_NOTICE ] )
string $error_msg – наша ошибка, например mysql_error()
int $error_type= E_USER_NOTICE
может принимать значение трех констант
E_USER_WARING,E_USER_NOTICE,E_USER_ERROR
Итак, перепишем наше сообщение об ошибках в одном из примеров с использованием данной функции:
Пример приведенный выше:
Результат работы в итоге будет тот же самым, скрипт завершит свое действие в той же строке при возникновении ошибки запроса, но с одним существенным отличием, теперь мы можем перехватить содержимое ошибки, и отправить его в лог файл ошибок.
Совет:
Директорию для хранения файлов с ошибками лучше всего определять за пределами директории вэб сервера. Либо ограничить доступ к ней с помощью .htaccess, или же хотя бы файлам с ошибками присвоить расширение .php и первой строчкой каждого файла сделать:
Чтобы было невозможно со страниц сайта прочесть содержимое файла, который будет нести столь важную информацию.
Вопрос: почему же перехватывать сообщения об ошибках именно в файл? Не в базу.
Ответ: ошибки могут возникнуть еще при подключении к базе данных, и их нужно корректно обработать и перехватить и на этом этапе тоже.
Чтобы перехватить вывод ошибок из браузера в файл мы воспользуемся следующими функциями.
mixed set_error_handler ( callback $error_handler [, int $error_types= E_ALL | E_STRICT ] )
Первым параметром функции set_error_handler указываем свою функцию перехватчик ошибок, вторым необязательным параметром указываем типы перехватываемых ошибок.
Функция, которая будет перехватывать ошибки, указываемая в параметре callback $error_handler должна принимать следующие параметры:
$error_num – номер ошибки,
$error_var – описание ошибки
$error_file – где произошла ошибка,
$error_line – строка, где произошла ошибка
Вот сокращенный пример перехвата ошибок в файл, работающий в нескольких моих вэб проектах. Добавляем этот код с первым подключаемым на каждой странице файлом, и тем самым организуем перехват ошибок в файлы. Кроме этого с ошибкой можно логировать достаточно много интересной информации, как например у меня – ай пи пользователя, страница, с которой он пришел на эту страницу, если таковая есть, время, когда произошла ошибка, и прочее.
Функциональность этого скрипта можно улучшить в сторону отсылки лога ошибок на указанный емейл, архивирования логов ошибок при достижении определенного размера файла ошибок и т.п.
Мне могут возразить, что на самом деле на серверах ведутся access_log, error_log логи доступа и ошибок вэб приложений. Но, не всегда есть доступ к этой информации, и свой перехватчик ошибок можно настроить гораздо гибче на предоставление именно той информации, которая нам нужна более всего.
Итак, после включения логирования информации и изменении вывода информации об ошибках в виде
на странице пользователя больше не отобразится информация о ошибке mysql, что добавит работы хакеру при выявлении возможности sql injection.
А в папке с ошибками появится файл с именем в виде текущего числа и содержащий следующую информацию:
Замечание:
Правда не все ошибки можно перехватить в PHP, например фатальные ошибки E_ERROR без хаков перехватить не получится.
Вторая проблема: недостаточная обработка данных перед помещением в базу данных.
Собственно здесь и возникают те самые ошибки, которые могут помочь удаленному пользователю произвести SQL injection.
Чтобы этого избежать советую перед помещением данных в запрос к базе применять фильтрацию данных.
Какие функции нам здесь могут помочь при проверке переменных перед построением запроса к базе данных?
Функции:
Для проверки существования непустых данных:
bool isset ( mixed $var [, mixed $var [, $... ]] )
Устанавливает определена ли переменная.
bool empty ( mixed $var )
Если переменная не должна быть пустой или не должна принимать отрицательное нулевое значение.
Для обработки целочисленных данных:
Функция
int intval ( mixed $var [, int $base ] )
Возвращает целое значение переменной var , используя указанное основание системы исчисления base для преобразования (основание по умолчанию 10).
bool is_numeric ( mixed $var )
Проверяет, является ли данная переменная числовой.
Если данные должны быть еще к тому же исключительно положительны (например: уникальный столбец в базе данных с атрибутом AUTO_INCREMENT) советую использовать:
number abs ( mixed $number )
Возвращает абсолютное значение number . Если number имеет тип float, возвращаемое значение также будет иметь тип float, иначе - integer.
Для обработки строковых данных:
Функция
string trim ( string $str [, string $charlist ] )
Удаляет лишние пробельные последовательности символов из начала и конца строки.
Функция string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier ] )
Советую обрабатывать данные этой функцией перед помещением их непосредственно в запрос к базе данных. Предпочтительнее использовать ее, нежели addslashes или доморощенные аналоги, дело в том, что данная функция корректно определяет кодировку подключения к mysql базе данных.
Но здесь не все так просто, нужно также будет избежать двойного экранирования строки. Воистину говорится «дорога в Ад вымощена благими намерениями». Так вот, чтобы сделать автоматическим экранирование данных разработчики PHP добавили такую возможность как magic quotes – или автоматическое экранирование данных. Эта возможность будет полностью исключена в 6 версии PHP. А пока с ней не знают что и делать. Часть хостингов ее отключает. Часть – нет. В чем же суть проблемы:
К данным, которые пришли в приложение из post, get массивов, если magic_quotes_gps = on применяется автоматически функция addslashes:
string addslashes ( string $str )
Возвращает сроку str , в которой перед каждым спецсимволом добавлен обратный слэш (\).
Пример:
Index.php?info=hi’there
В скрипте при включенном режиме magic_quotes_gps = on значение переменной $_GET[“info”] будет равно “hi\’there”
Добавится лишний \ бэк слэш. И если перед помещением данных в базу данных мы обработаем их с помощью mysql_real_escape_string то будет добавлен лишний слэш $_GET[“info”] будет равно “hi\\’there”
, что приведет к возникновению ошибки на стадии запроса.
Здесь нам поможет функция:
int get_magic_quotes_gpc ( void )
Которая вернет 1 если magic_quotes_gps включен. И мы сможем исключить лишние слэши из значения переменной с помощью функции:
string stripslashes ( string $str )
Удаляет экранирующие бэкслэши.
Index.php?info=hi’there
Примечание:
По существу magic_quotes могут испортить мультибайтовые данные в кодировке UTF-8, приходящие из клиента, приняв часть мультибайтной строки за спецсимвол. Эту директиву можно попробывать отключить напрямую из скрипта:
ini_set('magic_quotes_gpc',0);
Или используя, если разрешено хостером, .htaccess:
php_flag magic_quotes_gpc off
Кроме этого см. magic_quotes_runtime, get_magic_quotes_runtime(), set_magic_quotes_runtime(0)
Примечание:
В PHP до версти 4.3.0 аналог функции mysql_real_escape_string назывался mysql_escape_string.
Можно написать следующую функцию для проверки какая из функций поддерживается и с учетом всего вышесказанного:
Особенности работы с оператором LIKE
(Особенности работы с оператором LIKE по материалам интернета, статья Для просмотра ссылки Войдиили Зарегистрируйся)
Совершенно отдельный случай - оператор LIKE.
Во-первых, помимо обычного добавления бэк слеш, в переменных, которые подставляются в LIKE, надо удваивать слеши. То есть, если в переменной содержится символ \, то его надо удвоить, а после этого выполнить обычное добавление бэк слеш, через mysql_real_escape_string.
К примеру, если мы ищем строку:
и нам нужно точное совпадение, мы просто применяем mysql_real_escape_string и запрос получается стандартный:
Если же мы хотим подставить эту строку в LIKE, то сначала надо заменить каждый слеш на два, а потом применить mysql_real_escape_string. В результате получится:
Во-вторых, следует обратить внимание на то, что ни одна из функций, добавляющих слеши, не добавляет их к метасимволам поиска "%" и "_", используемым в операторе LIKE. Поэтому, если вы используете этот оператор, и не хотите, чтобы символы _ и % использовались, как маски, то добавляйте слеши вручную. Это можно сделать командой:
Внимание - это не addslashes! В имени этой функции есть дополнительная буква "c".
Таким образом получается, что переменные, используемые в операторе LIKE мы должны обрабатывать отдельно.
Сначала заменять один слеш на два, с помощью такого, к примеру, кода:
затем (можно наравне со всеми другими данными, идущими в запрос) прослешиваем:
а затем, если хотим, чтобы _ и % соответствовали точно самим себе, делаем
В результате, если мы будем искать, к примеру, такую строку
символ \ называется "backslash", а символ _ называется "underscore"
то после обработки, в запросе она должна выглядеть так:
'%символ \\\\ называется \"backslash\", а символ \_ называется \"underscore\"
То есть, слеш, который был в строке изначально - учетверился. Остальные символы прослешились, как обычно. Плюс - прослешился символ подчёркивания.
Избавляемся от XSS injection:
Какие функции могут помочь здесь?
string strip_tags ( string $str [, string $allowable_tags ] )
Эта функция возвращает строку str , из которой удалены HTML и PHP тэги.
Необязательный второй аргумент может быть использован для указания тэгов, которые не должны удаляться.
string htmlspecialchars ( string $string [, int $quote_style [, string $charset ]] ) – переводит в HTML -сущности амперсанд, кавычки, апостроф, знаки «больше» и «меньше».
string htmlentities ( string $string [, int $quote_style [, string $charset ]] ) – аналог htmlspecialchars, но использует HTML, -сущности
string html_entity_decode ( string $string [, int $quote_style [, string $charset ]] )
html_entity_decode(), в противоположность функции htmlentities(), Преобразует HTML сущности в строке string в соответствующие символы.
Рассмотрим самый общий вариант, получение исключительно текстовой информации из браузера клиента.
Примечание: данные, обработанные на предмет XSS инъекций все равно требуют обработки функцией mysql_real_escape_string перед формированием запроса к базе данных.
Иногда требуются более сложные фильтры содержимого, когда нужна гибкая обработка данных и мы должны «пропустить» нужный нам HTML контент, отфильтровав нежелательный .
Здесь требуется очень тщательно спланировать и задать себе хотя бы несколько подобных вопросов:
И в конце приведу как пример обработки данных для исключения xss угроз класс InputFilter использующийся в репозитарии cms Joomla v 1.5.9 (пример класса видоизменен для использования без пакета cms Joomla - можете брать и использовать в других своих проектах
Пример использования класса:
Проверка при загрузке файлов по http
Есть еще один вопрос, связанный с безопасностью, который стоит затронуть на мой взгляд. Это загрузка файлов по http.
Желательно после загрузки проверять файлы с помощью функции is_uploaded_file
bool is_uploaded_file ( string $filename )
Возвращает TRUE, если файл filename был загружен при помощи HTTP POST. Это полезно, чтобы убедиться в том, что злонамеренный пользователь не пытается обмануть скрипт так, чтобы он работал с файлами, с которыми работать не должен -- к примеру, /etc/passwd.
А так же проверять расширение загружаемого файла, чтобы удаленный пользователь не мог загрузить допустим файл с расширением php к вам на сервер, а потом выполнить его код.
Вот пример как можно сделать такую проверку:
Параметризированные запросы, placeholders.
Есть еще один способ отправлять запросы в БД, называемый "подготовленными выражениями" (prepared statements).
Суть его заключается в том, что подготавливается шаблон запроса, со специальными маркерами, на место которых будут подставлены динамические компоненты. Пример такого шаблона:
SELECT * FROM table WHERE name=?
Знак вопроса здесь - это тот самый маркер. По-другому он называетсй плейсхолдером (placeholder). Весь секрет в том, что данные на его место подставляет специальная функция, которая "привязывает" переменную к запросу.
Вот как выглядит код в таком случае:
Остановимся подробнее на коде этого примера.
$stmt = $mysqli->prepare("SELECT District FROM City WHERE Name=?");
$stmt->bind_param("s", $city);
Данная подстановка называется подстановкой на входе: значение переменной $city подставляется в текст запроса на место маркера ?.
mysqli_stmt_bind_param()
$stmt->bind_param ()
Выполняет подстановку значений переменных в запрос.
Аргументы:
Дескриптор запроса (только для функции)
Строка определяющая типы подстанавливаемых переменных (s - string, i-number,d-double,b-blob)
Список переменных
В четвертой строке выполняется собственно запрос.
Запрос и данные идут по отдельности, исключая возможность какой-либо ошибки или злонамеренной манипуляции.
См. документацию по библиотекам mysqli и PDO, реализующим данный принцип.
Так же можно использовать библиотеку DbSimple Дмитрия Котерова или PEAR:: DB. Основное отличие этих двух состоит в том, что они реализуют механизм подготовленных выражений только внешне. А внутри работают по-старинке - составляя запрос и отправляя его в базу, беря на себя работу по обработке переменных. А PDO и mysqli работают, как было описано выше - то есть, шаблон запроса и данные уходят в базу по отдельности.
Пример применения функции Sprintf для имитации параметризированных запросов от Для просмотра ссылки Войдиили Зарегистрируйся.
Функция:
string sprintf ( string $format [, mixed $args ] )
Возвращает строку, созданную с использованием строки формата format .
Описатель типа, определяющий, как трактовать тип данных аргумента. Допустимые типы:
В нашем примере описатель типа %d - целое число, которое будет заменено на величину $id перед запросом к СУБД и приведено к целому типу чисел.
Пример использования си подобных операторов для явного приведения типов при подготовке строки в виде конкатенции параметров:
PHP является слабо типизированным языком программирования. Но в нем присутствую операторы явного приведения типов.
Результатом будет:
Операнд записывается справа от оператора.
Пример подготовки запроса с использованием операторов приведения типов и конкатенции строки запроса и параметров:
Здесь тип присоединяемого к запросу параметра ай ди приводится к целому числу. А потом участвует в конкатенции запроса.
PS: весь приведенный код тестировался и работает (видоизмененно) в виде классов в некоторых моих проектах. Об ошибках, описках, дополнениях просьба писать ниже.
Содержание:
- Аннотация или суть проблемы
- Типы инъекций
- Code injection
- SQL injection
- XSS injection
- Советы по проектированию безопасного web приложения.
- Избавляемся от Code injection
- Избавялемся от SQL injection
- Сообщения об ошибках
- Обработка данных
- Оператор Like
- Избавляемся от XSS injection
- Проверка файлов при загрузке данных по http
- Mysqli параметризированные запросы, placeholders
Аннотация или суть проблеммы:
Приходится писать много кода. Иметь дело с огромным количеством вэб приложений. К сожалению, раз за разом среди совсем различных скриптов встречаю одни и те же ошибки, которые приводят к большим проблемам безопасности в среде разрабатываемого вэб приложения. Это и типичные ошибки, связанные с отсутствием или недостаточной фильтрацией данных, передаваемых от пользователя на сервер, и предоставление отладочной информации, информации об ошибках приложения и предупреждениях прямо в браузер на клиентской вэб странице, и недостаточная проверка содержимого файлов, загружаемых через POST.
Целью этой статьи будет вкратце ознакомить Вас с основными видами угроз безопасности web приложений и привести пример построения защиты стандартными средствами функций обработки данных и настройки выполнения скриптов в среде приложения, использующей для своего функционирования PHP 4+ Mysql 3.2+.
В основном благодаря различного рода уязвимостям и предоставлению отладочной информации на страницах поддерживаемого или разрабатываемого Вами вэб приложения удаленный пользователь, выполнив определенные действия, после поиска уязвимостей, сможет получить доступ практически к любой информации, циркулирующей внутри вашего скрипта, речь здесь ведется не только о нанесении урона конфиденциальности информации, как например потери данных о кредитных карточках, если мы имеем дело с интернет магазином, но и о возможности полного уничтожения вэб приложения и результатов его работы.
Согласитесь, не слишком радужная перспектива?
Примечание:
Статья от Вас потребует определенного уровня знаний. Вы должны быть как минимум поверхностно знакомы с базами данных и синтаксисом запросов SQL, иметь представление о работе с функциями в языке PHP, и о разметке HTML страниц, а так же о способе передачи информации от клиента к серверу.
Начнем с обзора основных уязвимостей:
Итак, какие виды уязвимостей бывают, об этом было довольно много сказано.
Рассмотрим три основные из них: Сode injection (php including), sql injection, xss scripting. (Краткая информация с описанием инъекций размещенная и дополненная здесь, взята со страниц журнала Для просмотра ссылки Войди
Сode injection
В теории все выглядит просто: есть скрипт, исполняемый на сервере, в который взломщику необходимо встроить свой код. Провернуть такую махинацию довольно просто, если соблюдены два условия. Во-первых, веб-разработчик должен использовать конструкцию include с параметром переменной, а во-вторых, должен плохо проверять данные, поступающие от пользователя. Наиболее часто такая ситуация возникает в простых скриптах:
Язык: HTML + PHP
PHP:
<!—- Заголовок -->
<?php
include ($page);
?>
<!—- Завершающая часть -->
Соответственно, работа идет со ссылками типа Для просмотра ссылки Войди
Листинг
Язык: PHP
PHP:
<?php
phpinfo();
?>
Создав такой файл у себя на сервере, просто включаем его в запрос вместо about.php – и видим всю информацию на экране. Таким образом, мы внедрили произвольный код в скрипт на сервере и получили необходимую информацию. Но это только цветочки, ведь можно при помощи PHP сделать все, что нашей душе угодно.
SQL Injection
Давным-давно, когда по земле еще ходили динозавры, веб-программеры использовали для хранения данных текстовые файлы. Потом, когда человек изобрел колесо, веб-разработчики придумали базы данных и стали хранить все в них. И было всем счастье – и список пользователей туда засунуть можно, и все документы на сайте положить. Но однажды один умелец случайно ввел в форму апостроф, и выдал скрипт SQL-ошибку. Прочитал умелец сообщение, подумал немного и ввел вместо апострофа
Листинг
• Язык: SQL
Код:
' OR '1'='1’
еще немного поколдовал и стал с тех пор администратором. Работа с базой ведется на языке SQL, например, чтобы проверить, что пользователь существует, можно сделать такой запрос по логину:
Листинг
• Язык: SQL
Код:
' SELECT * FROM users WHERE username='$username'
А теперь подставь в запрос апостроф или ' OR '1'='1’ и посмотри, что получится. Фактически, мы можем выполнить произвольный SQL-запрос, и случиться что-нибудь нехорошее:
Листинг
• Язык: SQL
Код:
' '; DELETE FROM customers WHERE 1 or username = '
Хочу кинуть еще пару хороших идей, как можно воспользоваться SQL-инъекцией. Для этого мы рассмотрим детали диалектов языка SQL у разных производителей. Начнем с MySQL, который делает то, что я от него ни как не ожидал . А проблема проста: если SELECT-запрос подвержен инъекции, то не факт, что его результат будет выведен на экран, но если внимательно почитать руководство по MySQL, то можно найти замечательный функционал – результат запроса можно перенаправить в файл!
Листинг
• Язык: SQL
Код:
SELECT <поля> FROM <таблица> INTO OUTFILE '<файл>';
Теперь осталось найти каталог, который нас приютит. В случае с CMS все решается довольно просто – почти всегда есть каталог для загрузки файлов upload. Именно в такой каталог и стоит перенаправлять вывод.
Начиная с четвертой версии, MySQL поддерживает объединение запросов при помощи команды UNION. Таким образом, можно вывести дополнительную информацию из произвольной таблицы. Посмотрим, как это выглядит на примере:
Листинг
• Язык: SQL
Код:
SELECT title, description FROM articles WHERE id=’$id’;
Title и description имеют тип varchar, поэтому переменной $id нужно присвоить такое значение, чтобы при подстановке получился следующий запрос:
Листинг
• Язык: SQL
Код:
SELECT title, description FROM articles WHERE id=’123123’
UNION
SELECT login, password FROM users;
/*
‘;
Обрати внимание, как я использовал комментарий – при Union-инъекциях – это стандартная практика отсечения ненужной части строки.
Примечание:
На самом деле здесь требуется выполнение еще одного условия, чтобы подобрать нужный тип запроса для SQL инъекций, необходимо чтобы разработчик использовал вывод сообщения об ошибках в стандартный поток браузера. Что практикуется очень часто.
Пример:
PHP:
<?php
$q = 'SELECT * FROM `bd` WHERE `id`='.$_GET['id'];
$r = mysql_query(q);
if(!is_resource($r)){ //!$r || mysql_error()
die(mysql_error()); //echo(mysql_error());
}
?>
Примерно вот так разработчик «выводит» возникшую при запросе к базе данных ошибку в браузер, что, зачастую, помогает злоумышленнику, предоставляя необходимую информацию для составления запроса с SQL injection.
XSS
В наше время на большинстве сайтов пользователи имеют возможность создавать свои материалы и комментировать чужие. Для красивого оформления пользователю дается возможность вводить данные в формате HTML. Ввод HTML-кода может быть разрешен напрямую, либо при помощи WYSIWYG-редактора. Казалось бы, все довольны! Особенно взломщики . Есть хорошая пословица: «Где HTML, там и JavaScript». Таким образом, при вводе можно использовать тег <script> для исполнения произвольного JavaScript (и не только его, кстати). А если мы можем использовать скрипты, которые исполняются на стороне пользователя, значит, мы можем украсть его куки! А тут уже и до взлома аккаунта недалеко…
Примечание:
Здесь не все так просто, получить можно не только cookie клиента, но можно изменить поведение клиентской страницы буквально произвольно, по желанию, от нарушения дизайна сайта, дефейса уязвимого сайта, вплоть до полного перенаправления всей информации с сайта источника на другой сайт. И дело не только в javascript, например без использования javascript вполне возможно добавить <iframe>, <frameset> и многие другие не желательные теги, включить flash тэги <embeded>, <object> и апллеты в html в содержимое страницы.
Фрагментированные XSS-атаки
Часто бывает так, что информация, вводимая пользователем, хорошо фильтруется, и к форме на кривой козе не подступишься. Но если в ней несколько полей, например, «название материалов» и «содержание материалов», то можно попробовать более сложный вид атаки – фрагментированный. При таком подходе, необходимо, чтобы данные из разных полей формы выводились последовательно, и тогда появляется вероятность того, что вместе «безобидные строчки» станут грозным оружием.
Советы по проектированию безопасного web приложения.
Избавляемся от Code injection:
Как обезопаситься? Здесь все просто. Передавайте через GET,POST массивы скрипты, которые нужно подключить в данном приложении, не напрямую, а косвенно, через определенную переменную, и, на основании ее значения, подключайте тот или иной скрипт, используя include или require.
Пример:
Нам нужно подключить содержимое файла example.php в index.php.
Передаем в GET переменную file значение 1.
Index.php?file=1
Код файла index.php:
PHP:
<?php
$file = intval($_GET['file']);
if(isset($file) && !empty($file)){
switch($file){
case 1:
include($_SERVER['DOCUMENT_ROOT'].'/ example.php');
break;
//case 2 case 3…. И так далее перечисление других подключаемых файлов
default:
break;
}
}
?>
Замечание:
Иногда угроза CODE injection возникает еще по одной причине. Некоторые начинающие вэб разработчики подключение самых обыкновенных файлов используют с помощью include,require, либо стараются придать конфигурационным файлам отличное от php расширение, .ini и прочее.
Предостерегаю вас от подобных ошибок. Для функции работы с файлами, пожалуйста, используйте набор стандартных функций для получения информации из файлов (file,file_get_contents,fread,fgetc,fpassthru,fgetss). Но не в коем случае не выполняйте их содержимое, как код PHP. Для конфигурационных файлов пожалуйста сохраняйте их с расширением .php. Web сервер сам поможет Вам скрыть содержимое данных файлов от просмотра через web браузер.
Избавляемся от SQL injection.
Первая проблема: Вывод информации об ошибках базы данных в браузер пользователя.
Нам нужно определиться, когда этап отладки миновал, стоит ли нам информацию об ошибках SQL, возникшую при работе скриптов, выводить в браузере клиента? По-моему нет. Но иногда требуется все же иметь доступ к этой информации, которая во многом в процессе работы приложения помогает устранять ошибки и недостатки. Как же быть?
Казалось бы, выход очевиден.
Используем попытку запрета вывода об ошибках вообще:
PHP:
<?php
ini_set('display_errors','off');
error_reporting(0);
?>
Но, ошибки базы данных, выводимые в браузер разработчиками с помощью
PHP:
echo(mysql_error()); die(mysql_error())
Не следует действительно писать:
PHP:
<?php
echo(mysql_error());
die(mysql_error())
?>
PHP предоставляет для этих случаев функцию trigger_error.
bool trigger_error ( string $error_msg [, int $error_type= E_USER_NOTICE ] )
string $error_msg – наша ошибка, например mysql_error()
int $error_type= E_USER_NOTICE
может принимать значение трех констант
E_USER_WARING,E_USER_NOTICE,E_USER_ERROR
Итак, перепишем наше сообщение об ошибках в одном из примеров с использованием данной функции:
Пример приведенный выше:
PHP:
<?php
$q = ‘SELECT * FROM `bd` WHERE `id`=’.$_GET[‘id’];
$r = mysql_query(q);
if(!is_resource($r)){ //!$r || mysql_error()
trigger_error(mysql_error(),E_USER_ERROR);
}
?>
Результат работы в итоге будет тот же самым, скрипт завершит свое действие в той же строке при возникновении ошибки запроса, но с одним существенным отличием, теперь мы можем перехватить содержимое ошибки, и отправить его в лог файл ошибок.
Совет:
Директорию для хранения файлов с ошибками лучше всего определять за пределами директории вэб сервера. Либо ограничить доступ к ней с помощью .htaccess, или же хотя бы файлам с ошибками присвоить расширение .php и первой строчкой каждого файла сделать:
PHP:
<?php die("");?>
Чтобы было невозможно со страниц сайта прочесть содержимое файла, который будет нести столь важную информацию.
Вопрос: почему же перехватывать сообщения об ошибках именно в файл? Не в базу.
Ответ: ошибки могут возникнуть еще при подключении к базе данных, и их нужно корректно обработать и перехватить и на этом этапе тоже.
Чтобы перехватить вывод ошибок из браузера в файл мы воспользуемся следующими функциями.
mixed set_error_handler ( callback $error_handler [, int $error_types= E_ALL | E_STRICT ] )
Первым параметром функции set_error_handler указываем свою функцию перехватчик ошибок, вторым необязательным параметром указываем типы перехватываемых ошибок.
Функция, которая будет перехватывать ошибки, указываемая в параметре callback $error_handler должна принимать следующие параметры:
$error_num – номер ошибки,
$error_var – описание ошибки
$error_file – где произошла ошибка,
$error_line – строка, где произошла ошибка
Вот сокращенный пример перехвата ошибок в файл, работающий в нескольких моих вэб проектах. Добавляем этот код с первым подключаемым на каждой странице файлом, и тем самым организуем перехват ошибок в файлы. Кроме этого с ошибкой можно логировать достаточно много интересной информации, как например у меня – ай пи пользователя, страница, с которой он пришел на эту страницу, если таковая есть, время, когда произошла ошибка, и прочее.
Функциональность этого скрипта можно улучшить в сторону отсылки лога ошибок на указанный емейл, архивирования логов ошибок при достижении определенного размера файла ошибок и т.п.
PHP:
<?php
define ('ROOT_PATH', $_SERVER['DOCUMENT_ROOT']."/");//корневая директория для файлов с ошибками
define('_ERR_HANDLING',true); //запретить - true/разрешить – false вывод ошибок в браузер
define('_ERR_DIR',ROOT_PATH.'/errs/'); //где будем хранить файлы ошибок, в какой папке?
err_handler();
function err_handler(){
if(_ERR_HANDLING){
$error_reporting = '';
$error_reporting = ini_get('error_reporting');
$error_reporting = $error_reporting?$error_reporting:E_ALL;
error_reporting(E_ERROR);
$date_file = date('dmY').'.php';
$dir = _ERR_DIR;
$path = $dir.$date_file;
$logfile = '';
if(!is_dir($dir) || !is_writable($dir)){
if(is_dir($dir)&&!is_writable($dir)){
chmod($dir,0775);
} else if(!is_dir($dir)){
$isdir = false;
$isdir = mkdir($dir,0775);
}
if(!$isdir&&!is_writable($dir)){
$dir = ROOT_PATH;
$path = $date_file;
}
}
if(is_dir($dir) && is_writable($dir)){
if(!is_file($path)){
$fp = fopen($path,'w+');
if($fp && is_resource($fp)){
$secuire = '<?php die("Forbidden."); ?>';
flock($fp,LOCK_EX);
fwrite($fp,$secuire."\n");
flock($fp,LOCK_UN);
fclose($fp);
$fp = null;
unset($secuire);
}
}
if(is_file($path) && !is_writable($path)){
chmod($path,0775);
}
if(is_file($path) && is_writable($path)){
ini_set('display_errors',0);
set_error_handler('error_reporting_log', (E_ALL & ~E_NOTICE));
$logfile = $path;
define('LOG_FILE',$logfile);
}
unset($date_file,$dir,$path,$logfile);
}
}
error_reporting($error_reporting);
unset($error_reporting);
}
function error_reporting_log($error_num, $error_var=null, $error_file=null, $error_line=null) {
$error_desc = '';
$error_desc = 'Error';
switch ($error_num){
case E_WARNING:
$error_desc = 'E_WARNING';
break;
case E_USER_WARNING:
$error_desc = 'E_USER_WARNING';
break;
case E_NOTICE:
$error_desc = 'E_NOTICE';
break;
case E_USER_NOTICE:
$error_desc = 'E_USER_NOTICE';
break;
case E_USER_ERROR:
$error_desc = 'E_USER_ERROR';
break;
default:
$error_desc = 'E_ALL';
break;
}
$date_file = date('y-m-d H:I:S');
$logfile = LOG_FILE;
$url = $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
$date_time = date('d.m.y - H:i:s');
$ip = $_SERVER['REMOTE_ADDR'];
$from = isset($_SERVER['HTTP_REFERER'])&&!empty($_SERVER['HTTP_REFERER'])?$_SERVER['HTTP_REFERER']:'';
$errortext = $error_desc.': '.$error_var."\t".'Line: '.$error_line."\t".'File: '.$error_file."\t".'Link: '.$url."\t".'Date: '.$date_time."\t".'IP: '.$ip."\t".' FROM:'.$from."\n";
unset($from,$error_desc,$error_var,$error_line,$error_file,$url,$date_time,$error_write);
$secuire = '<?php die("Forbidden."); ?>';
if(is_file($logfile)&&is_writeable($logfile)){
$strings = file($logfile);
if(isset($strings[0])&&!empty($strings[0])&&strpos($strings[0],$secuire)===false){
unlink($logfile);
}
unset($strings);
}
if(!is_file($logfile)){
$dir = dirname($logfile);
if(is_dir($dir)&&is_writable($dir)){
$fp = fopen($logfile,'w+');
if(is_resource($fp)){
flock($fp,LOCK_EX);
fwrite($fp,$secuire."\n");
flock($fp,LOCK_UN);
fclose($fp);
$fp = null;
}
unset($dir,$fp);
}
}
unset($secuire);
if(is_file($logfile)&&!is_writable($logfile)){
chmod($logfile,0775);
}
if(is_file($logfile)&&is_writeable($logfile)){
$fp = fopen($logfile,'a+');
if(is_resource($fp)){
flock($fp,LOCK_EX);
fwrite($fp,$errortext);
flock($fp,LOCK_UN);
fclose($fp);
$fp = null;
unset($fp);
}
}
unset($logfile);
return true;
}
?>
Мне могут возразить, что на самом деле на серверах ведутся access_log, error_log логи доступа и ошибок вэб приложений. Но, не всегда есть доступ к этой информации, и свой перехватчик ошибок можно настроить гораздо гибче на предоставление именно той информации, которая нам нужна более всего.
Итак, после включения логирования информации и изменении вывода информации об ошибках в виде
PHP:
trigger_error(mysql_error(),E_USER_ERROR);
А в папке с ошибками появится файл с именем в виде текущего числа и содержащий следующую информацию:
PHP:
<?php die("Forbidden"); ?>
E_USER_WARNING: MySQL error: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 1 Query:SELECT * FROM mybb_attachments WHERE pid IN () Line: 348 File: /forum/inc/db_mysql.php Link: */forum/archive/index.php/thread-796.html Date: 27.02.09 - 07:23:07 IP: 66.249.72.99 FROM: http://www.ya.ru
Замечание:
Правда не все ошибки можно перехватить в PHP, например фатальные ошибки E_ERROR без хаков перехватить не получится.
Вторая проблема: недостаточная обработка данных перед помещением в базу данных.
Собственно здесь и возникают те самые ошибки, которые могут помочь удаленному пользователю произвести SQL injection.
Чтобы этого избежать советую перед помещением данных в запрос к базе применять фильтрацию данных.
Какие функции нам здесь могут помочь при проверке переменных перед построением запроса к базе данных?
Функции:
Для проверки существования непустых данных:
bool isset ( mixed $var [, mixed $var [, $... ]] )
Устанавливает определена ли переменная.
bool empty ( mixed $var )
Если переменная не должна быть пустой или не должна принимать отрицательное нулевое значение.
Для обработки целочисленных данных:
Функция
int intval ( mixed $var [, int $base ] )
Возвращает целое значение переменной var , используя указанное основание системы исчисления base для преобразования (основание по умолчанию 10).
bool is_numeric ( mixed $var )
Проверяет, является ли данная переменная числовой.
Если данные должны быть еще к тому же исключительно положительны (например: уникальный столбец в базе данных с атрибутом AUTO_INCREMENT) советую использовать:
number abs ( mixed $number )
Возвращает абсолютное значение number . Если number имеет тип float, возвращаемое значение также будет иметь тип float, иначе - integer.
Для обработки строковых данных:
Функция
string trim ( string $str [, string $charlist ] )
Удаляет лишние пробельные последовательности символов из начала и конца строки.
Функция string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier ] )
Советую обрабатывать данные этой функцией перед помещением их непосредственно в запрос к базе данных. Предпочтительнее использовать ее, нежели addslashes или доморощенные аналоги, дело в том, что данная функция корректно определяет кодировку подключения к mysql базе данных.
Но здесь не все так просто, нужно также будет избежать двойного экранирования строки. Воистину говорится «дорога в Ад вымощена благими намерениями». Так вот, чтобы сделать автоматическим экранирование данных разработчики PHP добавили такую возможность как magic quotes – или автоматическое экранирование данных. Эта возможность будет полностью исключена в 6 версии PHP. А пока с ней не знают что и делать. Часть хостингов ее отключает. Часть – нет. В чем же суть проблемы:
К данным, которые пришли в приложение из post, get массивов, если magic_quotes_gps = on применяется автоматически функция addslashes:
string addslashes ( string $str )
Возвращает сроку str , в которой перед каждым спецсимволом добавлен обратный слэш (\).
Пример:
Index.php?info=hi’there
В скрипте при включенном режиме magic_quotes_gps = on значение переменной $_GET[“info”] будет равно “hi\’there”
Добавится лишний \ бэк слэш. И если перед помещением данных в базу данных мы обработаем их с помощью mysql_real_escape_string то будет добавлен лишний слэш $_GET[“info”] будет равно “hi\\’there”
, что приведет к возникновению ошибки на стадии запроса.
Здесь нам поможет функция:
int get_magic_quotes_gpc ( void )
Которая вернет 1 если magic_quotes_gps включен. И мы сможем исключить лишние слэши из значения переменной с помощью функции:
string stripslashes ( string $str )
Удаляет экранирующие бэкслэши.
Index.php?info=hi’there
PHP:
<?php
$magic_quotes = get_magic_quotes_gpc()?1:0;
$info = $_GET['info'];
if($magic_quotes){
$info = stripslashes($info);
}
$info = mysql_real_escape_string($info);
?>
Примечание:
По существу magic_quotes могут испортить мультибайтовые данные в кодировке UTF-8, приходящие из клиента, приняв часть мультибайтной строки за спецсимвол. Эту директиву можно попробывать отключить напрямую из скрипта:
ini_set('magic_quotes_gpc',0);
Или используя, если разрешено хостером, .htaccess:
php_flag magic_quotes_gpc off
Кроме этого см. magic_quotes_runtime, get_magic_quotes_runtime(), set_magic_quotes_runtime(0)
Примечание:
В PHP до версти 4.3.0 аналог функции mysql_real_escape_string назывался mysql_escape_string.
Можно написать следующую функцию для проверки какая из функций поддерживается и с учетом всего вышесказанного:
PHP:
<?php
function safe_sql($param){
if(is_array($param)){
$param = array_map('safe_sql',$param);
return $param;
} else if(is_string($param)){
$magic_quotes = get_magic_quotes_gpc()?1:0;
if($magic_quotes){
$param = stripslashes($param);
};
unset($magic_quotes);
if(function_exists('mysql_real_escape_string')){
$param = mysql_real_escape_string($param);
} else if(function_exists('mysql_escape_string')){
$param = mysql_escape_string($param);
}
return $param;
} else {
return $param; //не строка и не массив?
};
}
$info = safe_sql($_GET['info']);
?>
Особенности работы с оператором LIKE
(Особенности работы с оператором LIKE по материалам интернета, статья Для просмотра ссылки Войди
Совершенно отдельный случай - оператор LIKE.
Во-первых, помимо обычного добавления бэк слеш, в переменных, которые подставляются в LIKE, надо удваивать слеши. То есть, если в переменной содержится символ \, то его надо удвоить, а после этого выполнить обычное добавление бэк слеш, через mysql_real_escape_string.
К примеру, если мы ищем строку:
Код:
символ \ называется "backslash"
и нам нужно точное совпадение, мы просто применяем mysql_real_escape_string и запрос получается стандартный:
Код:
SELECT * FROM test WHERE field = 'символ \\ называется \"backslash\"'
Если же мы хотим подставить эту строку в LIKE, то сначала надо заменить каждый слеш на два, а потом применить mysql_real_escape_string. В результате получится:
Код:
SELECT * FROM table WHERE field LIKE '%символ \\\\ называется \"backslash\"%'
Во-вторых, следует обратить внимание на то, что ни одна из функций, добавляющих слеши, не добавляет их к метасимволам поиска "%" и "_", используемым в операторе LIKE. Поэтому, если вы используете этот оператор, и не хотите, чтобы символы _ и % использовались, как маски, то добавляйте слеши вручную. Это можно сделать командой:
PHP:
<?php
$data = addCslashes($data, '%_');
?>
Таким образом получается, что переменные, используемые в операторе LIKE мы должны обрабатывать отдельно.
Сначала заменять один слеш на два, с помощью такого, к примеру, кода:
PHP:
<?php
$var=str_replace('\\','\\\\',$var);
?>
PHP:
<?php
$var=mysql_real_escape_string($var);
?>
PHP:
$var=addCslashes($var, '_%');
В результате, если мы будем искать, к примеру, такую строку
символ \ называется "backslash", а символ _ называется "underscore"
то после обработки, в запросе она должна выглядеть так:
'%символ \\\\ называется \"backslash\", а символ \_ называется \"underscore\"
То есть, слеш, который был в строке изначально - учетверился. Остальные символы прослешились, как обычно. Плюс - прослешился символ подчёркивания.
Избавляемся от XSS injection:
Какие функции могут помочь здесь?
string strip_tags ( string $str [, string $allowable_tags ] )
Эта функция возвращает строку str , из которой удалены HTML и PHP тэги.
Необязательный второй аргумент может быть использован для указания тэгов, которые не должны удаляться.
string htmlspecialchars ( string $string [, int $quote_style [, string $charset ]] ) – переводит в HTML -сущности амперсанд, кавычки, апостроф, знаки «больше» и «меньше».
string htmlentities ( string $string [, int $quote_style [, string $charset ]] ) – аналог htmlspecialchars, но использует HTML, -сущности
string html_entity_decode ( string $string [, int $quote_style [, string $charset ]] )
html_entity_decode(), в противоположность функции htmlentities(), Преобразует HTML сущности в строке string в соответствующие символы.
Рассмотрим самый общий вариант, получение исключительно текстовой информации из браузера клиента.
PHP:
<?php
function get_text($param,$enc = 'UTF-8'){
$param = html_entity_decode($param,ENT_QUOTES,$enc);
$param = preg_replace('/&#(\d+);/me', "chr(\\1)", $param); // decimal notation
// convert hex
$param = preg_replace('/&#x([a-f0-9]+);/mei', "chr(0x\\1)", $param); // hex notation
// convert decimal
$param = strip_tags($param);
$param = trim($param);
//$param = nl2br($param);
return $param;
}
$param = get_text($_POST[‘param’]);
?>
Примечание: данные, обработанные на предмет XSS инъекций все равно требуют обработки функцией mysql_real_escape_string перед формированием запроса к базе данных.
Иногда требуются более сложные фильтры содержимого, когда нужна гибкая обработка данных и мы должны «пропустить» нужный нам HTML контент, отфильтровав нежелательный .
Здесь требуется очень тщательно спланировать и задать себе хотя бы несколько подобных вопросов:
- Какие данные являются допустимыми (тэги и прочее)
- Какие атрибуты этих данных требуют дополнительной проверки (как например свойства src тэгов
- Проверка данным на соответствие определенным критериям (как максимальная длинна строки и т.п.)
- Обработка данных для безопасного хранения в составе базы данных
И в конце приведу как пример обработки данных для исключения xss угроз класс InputFilter использующийся в репозитарии cms Joomla v 1.5.9 (пример класса видоизменен для использования без пакета cms Joomla - можете брать и использовать в других своих проектах
PHP:
<?php
/**
* @class: InputFilter (PHP4 & PHP5, with comments)
* @project: PHP Input Filter
* @date: 10-05-2005
* @version: 1.2.2_php4/php5
* @author: Daniel Morris
* @contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris
* Tobin and Andrew Eddie.
*
* Modification by Louis Landry
*
* @copyright: Daniel Morris
* @email: dan@rootcube.com
* @license: GNU General Public License (GPL)
*/
class InputFilter
{
var $tagsArray; // default = empty array
var $attrArray; // default = empty array
var $tagsMethod; // default = 0
var $attrMethod; // default = 0
var $xssAuto; // default = 1
var $tagBlacklist = array ('applet', 'body', 'bgsound', 'base', 'basefont', 'embed', 'frame', 'frameset', 'head', 'html', 'id', 'iframe', 'ilayer', 'layer', 'link', 'meta', 'name', 'object', 'script', 'style', 'title', 'xml');
var $attrBlacklist = array ('action', 'background', 'codebase', 'dynsrc', 'lowsrc'); // also will strip ALL event handlers
/**
* Constructor for inputFilter class. Only first parameter is required.
*
* @access protected
* @param array $tagsArray list of user-defined tags
* @param array $attrArray list of user-defined attributes
* @param int $tagsMethod WhiteList method = 0, BlackList method = 1
* @param int $attrMethod WhiteList method = 0, BlackList method = 1
* @param int $xssAuto Only auto clean essentials = 0, Allow clean
* blacklisted tags/attr = 1
*/
function inputFilter($tagsArray = array (), $attrArray = array (), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1)
{
/*
* Make sure user defined arrays are in lowercase
*/
$tagsArray = array_map('strtolower', (array) $tagsArray);
$attrArray = array_map('strtolower', (array) $attrArray);
/*
* Assign member variables
*/
$this->tagsArray = $tagsArray;
$this->attrArray = $attrArray;
$this->tagsMethod = $tagsMethod;
$this->attrMethod = $attrMethod;
$this->xssAuto = $xssAuto;
}
/**
* Method to be called by another php script. Processes for XSS and
* specified bad code.
*
* @access public
* @param mixed $source Input string/array-of-string to be 'cleaned'
* @return mixed $source 'cleaned' version of input parameter
*/
function process($source)
{
/*
* Are we dealing with an array?
*/
if (is_array($source))
{
$source = array_map( array( 'InputFilter', 'process' ), $source );
return $source;
} else
/*
* Or a string?
*/
if (is_string($source) && !empty ($source))
{
// filter source for XSS and other 'bad' code etc.
$source = $this->remove($this->decode($source));
return $source;
} else
{
/*
* Not an array or string.. return the passed parameter
*/
return $source;
}
}
/**
* Internal method to iteratively remove all unwanted tags and attributes
*
* @access protected
* @param string $source Input string to be 'cleaned'
* @return string $source 'cleaned' version of input parameter
*/
function remove($source)
{
$loopCounter = 0;
/*
* Iteration provides nested tag protection
*/
while ($source != $this->filterTags($source))
{
$source = $this->filterTags($source);
$loopCounter ++;
}
return $source;
}
/**
* Internal method to strip a string of certain tags
*
* @access protected
* @param string $source Input string to be 'cleaned'
* @return string $source 'cleaned' version of input parameter
*/
function filterTags($source)
{
/*
* In the beginning we don't really have a tag, so everything is
* postTag
*/
$preTag = null;
$postTag = $source;
/*
* Is there a tag? If so it will certainly start with a '<'
*/
$tagOpen_start = strpos($source, '<');
while ($tagOpen_start !== false)
{
/*
* Get some information about the tag we are processing
*/
$preTag .= substr($postTag, 0, $tagOpen_start);
$postTag = substr($postTag, $tagOpen_start);
$fromTagOpen = substr($postTag, 1);
$tagOpen_end = strpos($fromTagOpen, '>');
/*
* Let's catch any non-terminated tags and skip over them
*/
if ($tagOpen_end === false)
{
$postTag = substr($postTag, $tagOpen_start +1);
$tagOpen_start = strpos($postTag, '<');
continue;
}
/*
* Do we have a nested tag?
*/
$tagOpen_nested = strpos($fromTagOpen, '<');
$tagOpen_nested_end = strpos(substr($postTag, $tagOpen_end), '>');
if (($tagOpen_nested !== false) && ($tagOpen_nested < $tagOpen_end))
{
$preTag .= substr($postTag, 0, ($tagOpen_nested +1));
$postTag = substr($postTag, ($tagOpen_nested +1));
$tagOpen_start = strpos($postTag, '<');
continue;
}
/*
* Lets get some information about our tag and setup attribute pairs
*/
$tagOpen_nested = (strpos($fromTagOpen, '<') + $tagOpen_start +1);
$currentTag = substr($fromTagOpen, 0, $tagOpen_end);
$tagLength = strlen($currentTag);
$tagLeft = $currentTag;
$attrSet = array ();
$currentSpace = strpos($tagLeft, ' ');
/*
* Are we an open tag or a close tag?
*/
if (substr($currentTag, 0, 1) == "/")
{
// Close Tag
$isCloseTag = true;
list ($tagName) = explode(' ', $currentTag);
$tagName = substr($tagName, 1);
} else
{
// Open Tag
$isCloseTag = false;
list ($tagName) = explode(' ', $currentTag);
}
/*
* Exclude all "non-regular" tagnames
* OR no tagname
* OR remove if xssauto is on and tag is blacklisted
*/
if ((!preg_match("/^[a-z][a-z0-9]*$/i", $tagName)) || (!$tagName) || ((in_array(strtolower($tagName), $this->tagBlacklist)) && ($this->xssAuto)))
{
$postTag = substr($postTag, ($tagLength +2));
$tagOpen_start = strpos($postTag, '<');
// Strip tag
continue;
}
/*
* Time to grab any attributes from the tag... need this section in
* case attributes have spaces in the values.
*/
while ($currentSpace !== false)
{
$fromSpace = substr($tagLeft, ($currentSpace +1));
$nextSpace = strpos($fromSpace, ' ');
$openQuotes = strpos($fromSpace, '"');
$closeQuotes = strpos(substr($fromSpace, ($openQuotes +1)), '"') + $openQuotes +1;
/*
* Do we have an attribute to process? [check for equal sign]
*/
if (strpos($fromSpace, '=') !== false)
{
/*
* If the attribute value is wrapped in quotes we need to
* grab the substring from the closing quote, otherwise grab
* till the next space
*/
if (($openQuotes !== false) && (strpos(substr($fromSpace, ($openQuotes +1)), '"') !== false))
{
$attr = substr($fromSpace, 0, ($closeQuotes +1));
} else
{
$attr = substr($fromSpace, 0, $nextSpace);
}
} else
{
/*
* No more equal signs so add any extra text in the tag into
* the attribute array [eg. checked]
*/
$attr = substr($fromSpace, 0, $nextSpace);
}
// Last Attribute Pair
if (!$attr)
{
$attr = $fromSpace;
}
/*
* Add attribute pair to the attribute array
*/
$attrSet[] = $attr;
/*
* Move search point and continue iteration
*/
$tagLeft = substr($fromSpace, strlen($attr));
$currentSpace = strpos($tagLeft, ' ');
}
/*
* Is our tag in the user input array?
*/
$tagFound = in_array(strtolower($tagName), $this->tagsArray);
/*
* If the tag is allowed lets append it to the output string
*/
if ((!$tagFound && $this->tagsMethod) || ($tagFound && !$this->tagsMethod))
{
/*
* Reconstruct tag with allowed attributes
*/
if (!$isCloseTag)
{
// Open or Single tag
$attrSet = $this->filterAttr($attrSet);
$preTag .= '<'.$tagName;
for ($i = 0; $i < count($attrSet); $i ++)
{
$preTag .= ' '.$attrSet[$i];
}
/*
* Reformat single tags to XHTML
*/
if (strpos($fromTagOpen, "</".$tagName))
{
$preTag .= '>';
} else
{
$preTag .= ' />';
}
} else
{
// Closing Tag
$preTag .= '</'.$tagName.'>';
}
}
/*
* Find next tag's start and continue iteration
*/
$postTag = substr($postTag, ($tagLength +2));
$tagOpen_start = strpos($postTag, '<');
}
/*
* Append any code after the end of tags and return
*/
if ($postTag != '<')
{
$preTag .= $postTag;
}
return $preTag;
}
/**
* Internal method to strip a tag of certain attributes
*
* @access protected
* @param array $attrSet Array of attribute pairs to filter
* @return array $newSet Filtered array of attribute pairs
*/
function filterAttr($attrSet)
{
/*
* Initialize variables
*/
$newSet = array ();
/*
* Iterate through attribute pairs
*/
for ($i = 0; $i < count($attrSet); $i ++)
{
/*
* Skip blank spaces
*/
if (!$attrSet[$i])
{
continue;
}
/*
* Split into name/value pairs
*/
$attrSubSet = explode('=', trim($attrSet[$i]), 2);
list ($attrSubSet[0]) = explode(' ', $attrSubSet[0]);
/*
* Remove all "non-regular" attribute names
* AND blacklisted attributes
*/
if ((!eregi("^[a-z]*$", $attrSubSet[0])) || (($this->xssAuto) && ((in_array(strtolower($attrSubSet[0]), $this->attrBlacklist)) || (substr($attrSubSet[0], 0, 2) == 'on'))))
{
continue;
}
/*
* XSS attribute value filtering
*/
if ($attrSubSet[1])
{
// strips unicode, hex, etc
$attrSubSet[1] = str_replace('&#', '', $attrSubSet[1]);
// strip normal newline within attr value
$attrSubSet[1] = preg_replace('/\s+/', '', $attrSubSet[1]);
// strip double quotes
$attrSubSet[1] = str_replace('"', '', $attrSubSet[1]);
// [requested feature] convert single quotes from either side to doubles (Single quotes shouldn't be used to pad attr value)
if ((substr($attrSubSet[1], 0, 1) == "'") && (substr($attrSubSet[1], (strlen($attrSubSet[1]) - 1), 1) == "'"))
{
$attrSubSet[1] = substr($attrSubSet[1], 1, (strlen($attrSubSet[1]) - 2));
}
// strip slashes
$attrSubSet[1] = stripslashes($attrSubSet[1]);
}
/*
* Autostrip script tags
*/
if (InputFilter::badAttributeValue($attrSubSet))
{
continue;
}
/*
* Is our attribute in the user input array?
*/
$attrFound = in_array(strtolower($attrSubSet[0]), $this->attrArray);
/*
* If the tag is allowed lets keep it
*/
if ((!$attrFound && $this->attrMethod) || ($attrFound && !$this->attrMethod))
{
/*
* Does the attribute have a value?
*/
if ($attrSubSet[1])
{
$newSet[] = $attrSubSet[0].'="'.$attrSubSet[1].'"';
}
elseif ($attrSubSet[1] == "0")
{
/*
* Special Case
* Is the value 0?
*/
$newSet[] = $attrSubSet[0].'="0"';
} else
{
$newSet[] = $attrSubSet[0].'="'.$attrSubSet[0].'"';
}
}
}
return $newSet;
}
/**
* Function to determine if contents of an attribute is safe
*
* @access protected
* @param array $attrSubSet A 2 element array for attributes name,value
* @return boolean True if bad code is detected
*/
function badAttributeValue($attrSubSet)
{
$attrSubSet[0] = strtolower($attrSubSet[0]);
$attrSubSet[1] = strtolower($attrSubSet[1]);
return (((strpos($attrSubSet[1], 'expression') !== false) && ($attrSubSet[0]) == 'style') || (strpos($attrSubSet[1], 'javascript:') !== false) || (strpos($attrSubSet[1], 'behaviour:') !== false) || (strpos($attrSubSet[1], 'vbscript:') !== false) || (strpos($attrSubSet[1], 'mocha:') !== false) || (strpos($attrSubSet[1], 'livescript:') !== false));
}
/**
* Try to convert to plaintext
*
* @access protected
* @param string $source
* @return string Plaintext string
*/
function decode($source)
{
// url decode
$source = html_entity_decode($source, ENT_QUOTES);
// convert decimal
$source = preg_replace('/&#(\d+);/me', "chr(\\1)", $source); // decimal notation
// convert hex
$source = preg_replace('/&#x([a-f0-9]+);/mei', "chr(0x\\1)", $source); // hex notation
return $source;
}
}
?>
PHP:
<?php
$var = $_POST['var'];
$safeHtmlFilter = new InputFilter(null, null, 1, 1);
$var = $safeHtmlFilter->process($var);
//$var = nl2br($var);
?>
Проверка при загрузке файлов по http
Есть еще один вопрос, связанный с безопасностью, который стоит затронуть на мой взгляд. Это загрузка файлов по http.
Желательно после загрузки проверять файлы с помощью функции is_uploaded_file
bool is_uploaded_file ( string $filename )
Возвращает TRUE, если файл filename был загружен при помощи HTTP POST. Это полезно, чтобы убедиться в том, что злонамеренный пользователь не пытается обмануть скрипт так, чтобы он работал с файлами, с которыми работать не должен -- к примеру, /etc/passwd.
А так же проверять расширение загружаемого файла, чтобы удаленный пользователь не мог загрузить допустим файл с расширением php к вам на сервер, а потом выполнить его код.
Вот пример как можно сделать такую проверку:
PHP:
<?php
$valid = array('gif','png','jpg'); //через запятую указываем правильные расширения файлов, которые нужно “пропустить”
function _check_exts($file,$valid){
if(is_string($valid) && !empty($valid)){
$valid = array($valid);
}
if(!is_array($valid)){
return false;
}
$valid = array_map('trim',$valid);
$valid = array_map('strtolower',$valid);
$valid = array_unique($valid);
if(empty($file['tmp_name']) || empty($file['size'])) {
$file = null;
unset($file);
};
$is_upload = is_uploaded_file($file['tmp_name']);
if(!$is_upload){
$file = null;
unset($file);
return false;
}
unset($is_upload);
$ext = false;
$ext = getExtByName($file['name']);
if(!$ext){
unset($file);
return false;
}
if(!in_array($ext,$valid)){
$file = null;
unset($file);
return false;
}
unset($valid);
return true;
}
function getExtByName($name){
$name = strtolower($name);
$pos = 0;
$ext = '';
if(($pos = strpos($name,'.')) !== false){
$arr = array();
$arr = explode('.',$name);
$sizeof = sizeof($arr);
$ext = strtolower($arr[$sizeof-1]);
}
return $ext?$ext:false;
}
$is_valid = _check_exts($_FILES['img']);
if(!$is_valid){
unset($_FILES['img']);
}
?>
Параметризированные запросы, placeholders.
Есть еще один способ отправлять запросы в БД, называемый "подготовленными выражениями" (prepared statements).
Суть его заключается в том, что подготавливается шаблон запроса, со специальными маркерами, на место которых будут подставлены динамические компоненты. Пример такого шаблона:
SELECT * FROM table WHERE name=?
Знак вопроса здесь - это тот самый маркер. По-другому он называетсй плейсхолдером (placeholder). Весь секрет в том, что данные на его место подставляет специальная функция, которая "привязывает" переменную к запросу.
Вот как выглядит код в таком случае:
PHP:
$mysqli = mysqli_connect("localhost","test","","world");
$stmt = $mysqli->prepare("SELECT District FROM City WHERE Name=?");
$stmt->bind_param("s", $city);
$stmt->execute();
Остановимся подробнее на коде этого примера.
$stmt = $mysqli->prepare("SELECT District FROM City WHERE Name=?");
$stmt->bind_param("s", $city);
Данная подстановка называется подстановкой на входе: значение переменной $city подставляется в текст запроса на место маркера ?.
mysqli_stmt_bind_param()
$stmt->bind_param ()
Выполняет подстановку значений переменных в запрос.
Аргументы:
Дескриптор запроса (только для функции)
Строка определяющая типы подстанавливаемых переменных (s - string, i-number,d-double,b-blob)
Список переменных
В четвертой строке выполняется собственно запрос.
Запрос и данные идут по отдельности, исключая возможность какой-либо ошибки или злонамеренной манипуляции.
См. документацию по библиотекам mysqli и PDO, реализующим данный принцип.
Так же можно использовать библиотеку DbSimple Дмитрия Котерова или PEAR:: DB. Основное отличие этих двух состоит в том, что они реализуют механизм подготовленных выражений только внешне. А внутри работают по-старинке - составляя запрос и отправляя его в базу, беря на себя работу по обработке переменных. А PDO и mysqli работают, как было описано выше - то есть, шаблон запроса и данные уходят в базу по отдельности.
Пример применения функции Sprintf для имитации параметризированных запросов от Для просмотра ссылки Войди
PHP:
<?php
$id = 123;
echo $sql = 'SELECT * FROM table WHERE id = ' . $id;
echo '<br />';
echo $sql = sprintf('SELECT * FROM table WHERE id = %d', $id);
echo '<br />';
echo '<br />';
$id = "' '; DELETE FROM customers";
echo $sql = 'SELECT * FROM table WHERE id = ' . $id;
echo '<br />';
echo $sql = sprintf('SELECT * FROM table WHERE id = %d', $id);
?>
Функция:
string sprintf ( string $format [, mixed $args ] )
Возвращает строку, созданную с использованием строки формата format .
Описатель типа, определяющий, как трактовать тип данных аргумента. Допустимые типы:
- % - символ процента. Аргумент не используется.
- b - аргумент трактуется как целое и выводится в виде двоичного числа.
- c - аргумент трактуется как целое и выводится в виде символа с соответствующим кодом ASCII.
- d - аргумент трактуется как целое и выводится в виде десятичного числа со знаком.
- e - аргумент трактуется как float и выводится в научной нотации (например 1.2e+2).
- u - аргумент трактуется как целое и выводится в виде десятичного числа без знака.
- f - аргумент трактуется как float и выводится в виде десятичного числа с плавающей точкой.
- o - аргумент трактуется как целое и выводится в виде восьмеричного числа.
- s - аргумент трактуется как строка.
- x - аргумент трактуется как целое и выводится в виде шестнадцатиричного числа (в нижнем регистре букв).
- X - аргумент трактуется как целое и выводится в виде шестнадцатиричного числа (в верхнем регистре букв).
В нашем примере описатель типа %d - целое число, которое будет заменено на величину $id перед запросом к СУБД и приведено к целому типу чисел.
Пример использования си подобных операторов для явного приведения типов при подготовке строки в виде конкатенции параметров:
PHP является слабо типизированным языком программирования. Но в нем присутствую операторы явного приведения типов.
Результатом будет:
- (int)(integer) - целое число
- (float),(real),(double) - вещественное число
- (string) - строка
- (bool),(boolean) - Логическое значение
- (array) - Массив
- (object) - Объект
Операнд записывается справа от оператора.
Пример подготовки запроса с использованием операторов приведения типов и конкатенции строки запроса и параметров:
PHP:
<?php
$id = $_GET['id'];
$sql = 'SELECT * FROM tbl WHERE id='.(int)$id;
?>
PS: весь приведенный код тестировался и работает (видоизмененно) в виде классов в некоторых моих проектах. Об ошибках, описках, дополнениях просьба писать ниже.