Фильтрация и проверка данных PHP. Частые ошибки

Материал предназначен в основном для начинающих веб-программистов.

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

Здесь я постараюсь описать как можно подробнее частые ошибки при фильтрации данных в PHP скрипте и дать простые советы как правильно выполнить фильтрацию данных.

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

Разбор полетов.

Фильтрация. Ошибка №1

Для числовых переменных используется такая проверка:

$number = $_GET['input_number'];
if (intval($number))
{
... выполняем SQL запрос ...
}


Почему она приведет к SQL инъекции? Дело в том, что пользователь может указать в переменной input_number значение:
1'+UNION+SELECT


В таком случаи проверка будет успешно пройдена, т.к. функция intval получает целочисленное значение переменной, т.е. 1, но в самой переменной $number ничего не изменилось, поэтому весь вредоносный код будет передан в SQL запрос.
Правильная фильтрация:
$number = intval($_GET['input_number']);
if ($number)
{
... выполняем SQL запрос ...
}


Конечно, условие может меняться, например если вам нужно получить только определенный диапазон:
if ($number >= 32 AND $number <= 65)



Если вы используете чекбоксы или мультиселекты с числовыми значениями, выполните такую проверку:
$checkbox_arr = array_map('intval', $_POST['checkbox']);


array_map
Так же встречаю фильтрацию в виде:
$number = htmlspecialchars(intval($_GET['input_number']));

htmlspecialchars
Или:
$number = mysql_escape_string(intval($_GET['input_number']));


mysql_escape_string

Ничего кроме улыбки это не может вызвать :)

Фильтрация. Ошибка №2.

Для стринг-переменных используется такая фильтрация:
$input_text = addslashes($_GET['input_text']);


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

Используйте функцию mysql_escape_string или mysql_real_escape_string, пример:
$input_text = mysql_escape_string($_GET['input_text']);


Если вы не предполагаете вхождение html тегов, то лучше всего сделать такую фильтрацию:
$input_text = strip_tags($_GET['input_text']);
$input_text = htmlspecialchars($input_text);
$input_text = mysql_escape_string($input_text);

strip_tags — убирает html теги.
htmlspecialchars — преобразует спец. символы в html сущности.
Так вы защитите себя от XSS атаки, помимо SQL инъекции.
Если же вам нужны html теги, но только как для вывода исходного кода, то достаточно использовать:
$input_text = htmlspecialchars($_GET['input_text']);
$input_text = mysql_escape_string($input_text);



Если вам важно, чтобы значение переменной не было пустой, то используйте функцию trim, пример:
$input_text = trim($_GET['input_text']);
$input_text = htmlspecialchars($input_text);
$input_text = mysql_escape_string($input_text);



Фильтрация. Ошибка №3.

Она касается поиска в БД.
Для поиска по числам используйте фильтрацию, описанную в первой ошибке.
Для поиска по тексту используйте фильтрацию, описанную во второй ошибке, но с оговорками.
Для того, чтобы пользователь не смог выполнить логическую ошибку, нужно удалять или экранировать спец. символы SQL.
Пример без доп. обработки строки:
$input_text = htmlspecialchars($_GET['input_text']); // Поиск: "%"
$input_text = mysql_escape_string($input_text);


На выходе у нас получится запрос вида:
... WHERE text_row LIKE '%".$input_text."%' ... // WHERE text_row LIKE '%%%'


Это значительно увеличит нагрузку на базу.
В своём скрипте я использую функцию, которая удаляет нежелательные мне символы из поиска:
function strip_data($text)
{
    $quotes = array ("\x27", "\x22", "\x60", "\t", "\n", "\r", "*", "%", "<", ">", "?", "!" );
    $goodquotes = array ("-", "+", "#" );
    $repquotes = array ("\-", "\+", "\#" );
    $text = trim( strip_tags( $text ) );
    $text = str_replace( $quotes, '', $text );
    $text = str_replace( $goodquotes, $repquotes, $text );
    $text = ereg_replace(" +", " ", $text);
            
    return $text;
}


Конечно, не все из выше перечисленных символов представляют опасность, но в моём случаи они не нужны, поэтому выполняю поиск и замену.
Пример использования фильтрации:
$input_text = strip_data($_GET['input_text']);
$input_text = htmlspecialchars($input_text);
$input_text = mysql_escape_string($input_text);


Также советую сделать ограничение по количеству символов в поиске, хотя бы не меньше 3-х, т.к. если у вас будет большое количество записей в базе, то поиск по 1-2 символам будет значительно увеличивать нагрузку на БД.

Фильтрация. Ошибка №4.

Не фильтруются значения в переменной $_COOKIE. Некоторые думаю, что раз эту переменную нельзя передать через форму, то это гарантия безопасности.
Данную переменную очень легко подделать любым браузером, отредактировав куки сайта.
Например, в одной известной CMS была проверка, используемого шаблона сайта:
if (@is_dir ( MAIN_DIR . '/template/' . $_COOKIE['skin'] )){
	$config['skin'] = $_COOKIE['skin'];
}
$tpl->dir = MAIN_DIR . '/template/' . $config['skin'];


В данном случаи можно подменить значение переменной $_COOKIE['skin'] и вызвать ошибку, в результате которой вы увидите абсолютный путь до папки сайта.
Если вы используете значение куков для сохранения в базу, то используйте одну из выше описанных фильтраций, тоже касается и переменной $_SERVER.

Фильтрация. Ошибка №5.

Включена директива register_globals. Обязательно выключите её, если она включена.
В некоторых ситуациях можно передать значение переменной, которая не должна была передаваться, например, если на сайте есть группы, то группе 2 переменная $group должна быть пустой или равняться 0, но достаточно подделать форму, добавив код:
<input type="text" name="group" value="5" />


В PHP скрипте переменная $group будет равна 5, если в скрипте она не была объявлена со значением по умолчанию.

Фильтрация. Ошибка №6.

Проверяйте загружаемые файлы.
Выполняйте проверку по следующим пунктам:
1. Расширение файла. Желательно запретить загрузку файлов с расширениями: php, php3, php4, php5 и т.п.
2. Загружен ли файл на сервер move_uploaded_file
3. Размер файла

Проверка. Ошибка №1.

Сталкивался со случаями, когда для AJAX запроса (например: повышение репутации) передавалось имя пользователя или его ID (кому повышается репутация), но в самом PHP не было проверки на существование такого пользователя.
Например:
$user_id = intval($_REQUEST['user_id']);
... INSERT INTO REPLOG SET uid = '{$user_id}', plus = '1' ...
... UPDATE Users SET reputation = reputation+1 WHERE user_id = '{$user_id}' ...


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

Проверка. Ошибка №2.

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

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

Проверка. Ошибка №3.

При использовании нескольких php файлов сделайте простую проверку.
В файле index.php (или в любом другом главном файле) напишите такую строчку перед подключением других php файлов:
define ( 'READFILE', true );


В начале других php файлов напишите:
if (! defined ( 'READFILE' ))
{
	exit ( "Error, wrong way to file.<br><a href=\"/\">Go to main</a>." );
}


Так вы ограничите доступ к файлам.

Проверка. Ошибка №4.

Используйте хеши для пользователей. Это поможет предотвратить вызов той или иной функции путём XSS.
Пример составления хеша для пользователей:
$secret_key = md5( strtolower( "http://site.ru/" . $member['name'] . sha1($password) . date( "Ymd" ) ) ); // $secret_key - это наш хеш


Далее во все важные формы подставляйте инпут со значением текущего хеша пользователя:
<input type="hidden" name="secret_key" value="$secret_key" />


Во время выполнения скрипта осуществляйте проверку:
if ($_POST['secret_key'] !== $secret_key)
{
exit ('Error: secret_key!');
}



Проверка. Ошибка №5.

При выводе SQL ошибок сделайте простое ограничение к доступу информации. Например задайте пароль для GET переменной:
if ($_GET['passsql'] == "password")
{
... вывод SQL ошибки ...
}
else
{
... Просто информация об ошибке, без подробностей ...
}


Это позволит скрыть от хакера информацию, которая может ему помочь во взломе сайта.

Проверка. Ошибка №5.
Старайтесь не подключать файлы, получая имена файлов извне.
Например:
if (isset($_GET['file_name']))
{
include $_GET['file_name'] .'.php';
}


Используйте переключатель switch:
switch($_GET['file_name'])
{         
         case 'file_1':
         include 'file_1.php';    
         break;     
         
         default:
         include 'file_0.php';    
         break;
}


В таком случаи вы предотвратите подключение файлов, которые не были вами предусмотрены.

Совет.

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

UPD: Поправил пост. Перенес все советы по поводу функций и переменных, которые были в комментариях.

Добавлено: 19 Ноября 2021 07:27:05 Добавил: Андрей Ковальчук

Php BB коды

Иногда нам хочется добавить изюминку к нашим скриптам, сохранять в базе сообщения от пользователей не только в простом текстовом формате, но и давать им возможность форматировать свои сообщения как текст html. Здесь и нужны так называемые BB коды. То есть, это придуманные нами парные теги, которые при использовании нами будут преобразовываться в html теги.

BB код Преобразование Описание
&#91;b&#93; что-то здесь &#91;/b&#93; <b>что-то здесь</b> жирный текст
&#91;i&#93; что-то здесь &#91;/i&#93; <i>что-то здесь</i> наклонный текст
&#91;u&#93; что-то здесь &#91;/u&#93; <u>что-то здесь</u> подчеркнутый текст
&#91;q&#93;что-то здесь &#91;/q&#93; <table><tr><td>
что-то здесь
</td></tr></table> выделенная цитата
&#91; list &#93; что-то здесь&#91;/list&#93; <ul><li>что-то здесь</li></ul> не нумерованный список
&#91;listn&#93;что-то здесь &#91;/listn&#93; <ol><li>что-то здесь</li></ol> нумерованный список
(url)что-то здесь (/url) <a href="">что-то здесь</a> Url ссылка
Ранее в разделе Php база, мы научились с Вами сохранять данные в таком формате, чтобы он не причинил вреда при выводе его из базы в html текст, теперь будем добавлять BB коды, а потом их преобразовывать в html. Пользователю, которого мы лишили html форматирования, теперь становится интереснее. Но, как обычно, в любом начинании есть проблемы.
Одна из проблем в том, что пользователь может забыть закрыть тег, или, ещё хуже, какой-нибудь нехороший пользователь специально захочет Вам навредить. Так вот, как найти парные теги в сообщении полученном из формы, которое пользователь послал в Гостевую книгу или в Форум? Дело в том, что если у Вас простая Гостевая, это не требуется, но если есть BB коды, это становится актуально, ведь потом эти парные теги мы будем заменять на их html эквиваленты:

К примеру:
&#91;b&#93; что-то здесь &#91;/b&#93;, посланное пользователем, преобразуется в
<b>что-то здесь</b>, в этих BB кодах

Теперь представьте, что пользователь по ошибке или специально открыл только один тег
&#91;b&#93; что-то здесь, а закрыть тег не захотел, то получится:
<b>что-то здесь, а закрыть тег не захотел, то получится

То есть, надо проверять парность тегов:
- убирать в начале текста закрывающийся тег &#91;/b&#93;;
- убирать в конце текста открывающийся тег &#91;b&#93;;
- убирать пустые парные теги &#91;b&#93; &#91;/b&#93;;
- убирать повторяющиеся теги &#91;b&#93;&#91;b&#93; или &#91;/b&#93;&#91;/b&#93;;

Создадим такую функцию и посмотрим как она работает:

<?php
    # функция, которая убирает мусор в парных тегах
function Teg ($one,$teg,$too,$path){
$on = preg_quote ($one,"~");
$to = preg_quote ($too,"~");
$saerch1 = preg_quote ($one,"~") . $teg . preg_quote ($too,"~");
$saerch2 = preg_quote ($one,"~") . "/" . $teg . preg_quote ($too,"~");
$path = preg_replace ("~[ ]+~"," ",$path); 
$path = trim ($path);
$path = preg_replace ("~(".$saerch1."[ ]?".$saerch1.")+~", $one.$teg.$too, $path); 
$path = preg_replace ("~(".$saerch2."[ ]?".$saerch2.")+~", $one."/".$teg.$too, $path); 
$path = preg_replace ("~ˆ([ ]*".$saerch2.")*~","",$path); 
$path = preg_replace ("~(".$saerch1."[ ]*)*$~","",$path); 
$_search = "~".$saerch1."(.+)".$saerch2."~U";
$search = array ();
if ( preg_match_all ($_search, $path, $array, PREG_PATTERN_ORDER)){
while ( list (, $val) = each ($array[0])){
$content = "~" . preg_quote ($val,"~") . "~U";
if ( !empty ($search[0])){ if ($content==$search[0]){continue;} }
if ( @array_search ($content,$search)){ continue; }
$search[] = $content;
$pp = preg_replace ("~".$on."[/]?".$teg.$to."~","",$val);
if ( !preg_match ("~[a-zA-Z0-9а-яА-Я_]~",$pp)){ $pp = " "; }
else { $pp = $one.$teg.$too.$pp.$one."/".$teg.$too; }
$replace[] = $pp;
}
$search[] = "~0~e";
$replace[] = "0";
$path = preg_replace ($search, $replace, $path); 
$path = preg_replace ("~[ ]+~"," ",$path); 
$path = preg_replace ("~".$on."([/]?)".$teg.$to."[ ]?".$on."\\1".$teg.$to."~s", $one."\\1".$teg.$too, $path);
}
if ( !preg_match ($_search, $path)){ $path = preg_replace ("~".$on."[/]?".$teg.$to."~", '', $path); }
return $path;
} 

    # строка с плохими парными тегами
$string = " &#91;/b&#93; &#91;/b&#93; &#91;/b&#93;&#91;b&#93; Проб&#91;b&#93;у&#91;b&#93;&#91;b&#93;ем &#91;/b&#93;Обм&#91;b&#93;ан&#91;b&#93;уть &#91;b&#93;Програ &#91;/b&#93; &#91;/b&#93; &#91;/b&#93;мму &#91;b&#93; &#91;b&#93; &#91;b&#93; ";
echo $string."\n<br><br>";

    # применим функцию по сбору мусора
    # в ней четыре аргумента: первые три показывают
    # какой тег проверяем, а последний где проверяем
$string = Teg ("&#91;","b","&#93;",$string);
echo $string;

?>


В итоге работы программы получим:

&#91;/b&#93; &#91;/b&#93; &#91;/b&#93;&#91;b&#93; Проб&#91;b&#93;у&#91;b&#93;&#91;b&#93;ем &#91;/b&#93;Обм&#91;b&#93;ан&#91;b&#93;уть &#91;b&#93;Програ &#91;/b&#93; &#91;/b&#93; &#91;/b&#93;мму &#91;b&#93; &#91;b&#93; &#91;b&#93;

&#91;b&#93; Пробуем &#91;/b&#93;Обм&#91;b&#93;ануть Програ &#91;/b&#93;мму

То есть, в базу, при таком подходе мы запишем только парные теги. Вы скажете зачем всё это надо, но одно дело когда, например не закрылся тег &#91;b&#93;, ещё пол беды (весь тест далее будет выделен жирным шрифтом), и совсем другое когда не закрылся &#91;q&#93;, который мы будем преобразовывать в начало таблицы <table><tr><td> (представляете, что будет на странице при выводе такого не закрытого тега).

То есть, теперь подведем итог:
Пользователь послал сообщение $message, которое мы будем записывать в базу, как убрать html дескрипторы из сообщения мы знаем (раздел Php база), теперь оставим парные и уберем пустые теги BB кодов:

<?php 
    // оставим парные и уберем пустые теги 
$message = Teg ("&#91;","b","&#93;",$message); 
$message = Teg ("&#91;","i","&#93;",$message); 
$message = Teg ("&#91;","u","&#93;",$message); 
$message = Teg ("&#91;","q","&#93;",$message); 
$message = Teg ("&#91;","list","&#93;",$message); 
$message = Teg ("&#91;","listn","&#93;",$message); 
$message = Teg ("(","url",")",$message); 
    // записываем сообщение в базу 
$fp = fopen ("Base.dat","a"); 
fputs ($fp,"$message\n"); 
fclose ($fp); 
?>

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

Добавлено: 02 Апреля 2018 19:11:40 Добавил: Андрей Ковальчук

Комментарии

PHP поддерживает комметарии в стиле 'C', 'C++' и оболочки Unix (стиль Perl). Например:

<?php
    echo "Это тест"; // Это однострочный комментарий в стиле c++
    /* Это многострочный комментарий
       еще одна строка комментария */
    echo "Это еще один тест";
    echo "Последний тест"; # Это комментарий в стиле оболочки Unix
?>

Однострочные комментарии идут только до конца строки или текущего блока PHP-кода, в зависимости от того, что идет перед ними. Это означает, что HTML-код после // ... ?> или # ... ?> БУДЕТ напечатан: ?> завершает режим PHP и возвращает режим HTML, а // или # не могут повлиять на это. Если включена директива asp_tags, то аналогичное поведение будет и с // %> и # %>. Однако, тег </script> не завершает режим PHP в однострочном комментарии.
<h1>Это <?php # echo "простой";?> пример</h1>
<p>Заголовок вверху выведет 'Это пример'.</p>

'C'-комментарии заканчиваются при первой же обнаруженной последовательности */. Убедитесь, что вы не вкладываете друг в друга 'C'-комментарии. Очень легко допустить эту ошибку при комментировании большого блока кода. .
<?php
 /*
    echo "Это тест"; /* Этот комментарий вызовет проблему */
 */
?>

Добавлено: 09 Февраля 2015 21:07:06 Добавил: Андрей Ковальчук

Разделение инструкций

Как в C или Perl, PHP требует окончания инструкций точкой запятой в конце каждой инструкции. Закрывающий тег блока PHP-кода автоматически применяет точку с запятой; т.е. нет необходимости ставить точку с запятой в конце последней строки блока с PHP-кодом. Закрывающий тег блока "поглотит" немедленно следующий за ним переход на новую строку, если таковой будет обнаружен.

<?php
    echo 'Это тест';
?>

<?php echo 'Это тест' ?>

<?php echo 'Мы опустили последний закрывающий тег';

Замечание:
[QUOTE]Закрывающий тег PHP-блока в конце файла не является обязательным, и в некоторых случаях его опускание довольно полезно, например, при использовании include() или require(), так, что нежелательные пробелы не останутся в конце файла и вы все еще сможете добавить http-заголовки после подключения к ответу сервера. Это также удобно при использовании буферизации вывода, где также нежелательно иметь пробелы в конце частей ответа, сгенерированного подключаемыми файлами.[\/QUOTE]

Добавлено: 09 Февраля 2015 21:04:18 Добавил: Андрей Ковальчук

Вставка в HTML

Когда PHP обрабатывает файл, он ищет открывающие и закрывающие теги, которые указывают PHP, когда начинать и заканчивать обработку кода между ними. Подобный способ обработки позволяет PHP внедряться во все виды различных документов, так как всё, что находится вне пары открывающих и закрывающих тегов, будет проигнорировано парсером PHP. В большинстве случаев PHP внедряется в HTML-документы, как это показано в следующем примере.
<p>Это будет проигнорировано.</p>
<?php echo 'А это будет обработано.'; ?>
<p>Это тоже будет проигнорировано.</p>
Можно использовать и более продвинутые структуры:
Пример #1 Продвинутое внедрение

<?php
if ($expression) {
    ?>
    <strong>Это истина.</strong>
    <?php
} else {
    ?>
    <strong>Это ложь.</strong>
    <?php
}
?>

Это работает так, как и ожидается, потому что когда PHP встречает закрывающие теги ?>, он просто начинает выводить все, что встретит (за исключением перевода строки, стоящим сразу после закрывающего тега - смотрите разделение инструкций), пока не встретит следующий открывающий тег. Вышеуказанный пример, разумеется, надуман, но при больших объемах текста выход из режима PHP обычно более эффективен, чем посылка всего текста через echo() или print().
Существует четыре набора тегов, которые могут быть использованы для обозначения PHP-кода. Из них только два <?php ?> и <script language="php"> </script>) всегда доступны. Другими двумя являются короткие теги и теги в стиле ASP, которые могут быть включены или выключены в конфигурационном файле php.ini. Хотя короткие теги и теги в стиле ASP могут быть удобны, они не так переносимы, как длинные версии, и поэтому не рекомендуются.
Замечание:
Кроме того, если вы намереваетесь вставлять PHP-код в XML или XHTML, чтобы соответствовать XML стандартам, ам следует использовать форму <?php ?>.
Пример #2 Открывающие и закрывающие теги PHP
1.
<?php echo 'если вы хотите работать с документами XHTML или XML, делайте так'; ?>


2.
<script language="php">
        echo 'некоторые редакторы (например, FrontPage) не
              любят инструкции обработки';
    </script>


3.
<? echo 'это простейшая инструкция обработки SGML'; ?>
    <?= выражение ?> Это синоним для "<? echo выражение ?>"


4.
<% echo 'Вы можете по выбору использовать теги в стиле ASP'; %>
    <%= $variable; # Это синоним для "<% echo . . ." %>

Несмотря на то, что теги указанные в первых двух примерах всегда доступны, наиболее широко используется (и рекомендуется) первый пример из этих двух.
Короткие теги (третий пример) доступны только когда они включены с помощью директивы short_open_tag в конфигурационном файле php.ini, либо если PHP был скомпилирован с опцией --enable-short-tags .
Теги в стиле ASP (четвертый пример) доступны только когда они были с помощью директивы asp_tags в конфигурационном файле php.ini.
Замечание:
Следует избегать использования коротких тегов при разработке приложений или библиотек, предназначенных для распространения или размещения на PHP-серверах, не находящихся под вашим контролем, так как короткие теги могут не поддерживаться на целевом сервере. Для создания переносимого, совместимого кода, не используйте короткие теги.
Замечание:
В PHP 5.2 и более ранних версиях парсер не позволял файлам содержать только один открытый тег <?php. Это было разрешено, начиная с версии PHP 5.3.
Замечание:
Начиная с версии PHP 5.4, короткий тег вывода <?= распознается всегда, вне зависимости от значения директивы short_open_tag.

Добавлено: 09 Февраля 2015 20:58:12 Добавил: Андрей Ковальчук