Прийоми безпечного програмування веб-додатків на PHP, PHP, Програмування, статті

Ілля Басалаев (Club of the developers PHP)
www.citforum.ru

Дана стаття не претендує на роль всеосяжного керівництва на тему “як зробити так, щоб мене ніхто не поламав”. Так не буває. Єдина мета цієї статті – показати деякі використовувані мною прийоми для захисту веб-додатків типу WWW-чатів, гостьових книг, веб-форумів та інших програм подібного роду. Отже, давайте розглянемо деякі прийоми програмування на прикладі якоїсь гостьової книги, написаної на PHP.

Першою заповіддю веб-програміста, охочого написати більш-менш захищене веб-додаток, повинно стати “Ніколи не вір даним, що надсилаються тобі користувачем”. Користувачі – це за визначенням такі злісні хакери, які тільки і шукають моменту, як би напхати у форми введення всяку погань типу PHP, JavaScript, SSI, викликів своїх моторошно хакерських скриптів і тому подібних жахливих речей. Тому перше, що необхідно зробити – це найжорстокішим чином відфільтрувати всі дані, надіслані користувачем.

Припустимо, у нас в гостьовій книзі існує 3 форми вводу: ім’я користувача, його e-mail і само по собі тіло повідомлення. Перш за все, обмежимо кількість даних, переданих з форм введення чим-небудь на кшталт:

<input type=text name=username maxlength=20>

На роль цієї захисту, звичайно, це претендувати не може – єдине призначення цього елементу – обмежити користувача від випадкового введення імені довше 20-ти символів. А для того, щоб у користувача не виникло спокуси скачати документ із формами введення та підправити параметр maxlength, встановимо де-небудь на самому початку скрипта, що обробляє дані, перевірку змінної оточення web-сервера HTTP-REFERER:

<?
$referer=getenv("HTTP_REFERER");
if (!ereg("^http://www.myserver.com")) {
        echo "hacker? he-he...\n";
        exit;
}
?>

Тепер, якщо дані передані не з форм документа, що знаходиться на сервері www.myserver.com, хацкеру буде видано деморалізуючий повідомлення. Насправді, і це теж не може служити 100%-ой гарантією того, що дані ДІЙСНО передано з нашого документа. Зрештою, мінлива HTTP_REFERER формується браузером, і ніхто не може перешкодити хакеру підправити код браузера, або просто зайти Телнет на 80-й порт і сформувати свій запит. Так що подібний захист годиться тільки від Ну Зовсім Неосвічених хакерів. Втім, за моїми спостереженнями, близько 80% відсотків зловмисників на цьому етапі зупиняються і далі не лізуть – чи то IQ не дозволяє, чи то просто лінь. Особисто я просто виніс цей фрагмент коду в окремий файл, і викликаю його звідусіль, звідки це можливо. Часу на звернення до змінної йде небагато – а береженого Бог береже.

Наступним етапом стане горезвісна жорстка фільтрація переданих даних. Перш за все, не будемо довіряти змінної maxlength у формах введення і ручками поріжемо рядок:

$username=substr($username,0,20);

Не дамо користувачеві використовувати порожнє поле імені – просто так, щоб не давати писати анонімні повідомлення:

if (empty($username)) {
        echo "invalid username";
        exit;
}

Заборонимо користувачеві використовувати в своєму імені будь-які символи, крім букв російського і латинського алфавіту, знака “_” (підкреслення), пропусків і цифр:

if (preg_match("/[^(\w)|(\x7F-\xFF)|(\s)]/",$username)) {
        echo "invalid username";
        exit;
}

Я віддаю перевагу скрізь, де потрібно щось більш складне, ніж перевірити наявність патерну в рядку або поміняти один патерн на інший, використовувати Перл-сумісні регулярні вирази (Perl-compatible Regular Expressions). Те ж саме можна робити і використовуючи стандартні PHP-шні ereg () і eregi (). Я не буду приводити тут ці приклади – це досить докладно описано в мануалі.

Для поля введення адреси e-mail додамо в список дозволених символів знаки “@” і “.”, Інакше користувач не зможе коректно ввести адресу. Зате приберемо російські букви і пропуск:

if (preg_match("/[^(\w)|(\@)|(\.)]/",$usermail)) {
        echo "invalid mail";
        exit;
}

Поле введення тексту ми не будемо піддавати таким жорстким репресіям – перебирати всі знаки пунктуації, які можна використовувати, просто лінь, тому обмежимося використанням функцій nl2br () і htmlspecialchars () – Це не дасть ворогові понатикати в текст повідомлення html-тегів. Деякі розробники, напевно, скажуть: “а ми все-таки дуже хочемо, щоб користувачі _моглі_ вставляти теги”. Якщо сильно кортить – можна зробити якісь тегозаменітелі, типу “текст, оточений зірочками, буде висвітлений bold’ом.”. Але ніколи не слід дозволяти користувачам використання тегів, що припускають підключення зовнішніх ресурсів – Від тривіального до супернавороченного .

Якось раз мене попросили потестувати html-чат. Першим же поміченим мною багом було саме дозвіл вставки картинок. Враховуючи ще пару особливостей будови чату, через кілька хвилин у мене був файл, в якому акуратно були перераховані IP-адреси, імена і паролі всіх присутніх у цей момент на чаті користувачів. Як? Та дуже просто – чату був посланий тег , в результаті чого браузери всіх користувачів, присутніх в той момент на чаті, викликали скрипт myscript.pl з хоста myserver.com. (Там не було людей, що сиділи під lynx’ом :-)). А скрипт, перед тим як видати location на картинку, звалив мені в лог-файл половину змінних оточення – зокрема QUERY_STRING, REMOTE_ADDR та інших. Для кожного користувача. З вищезазначеним результатом.

Тому моя думка – так, дозволити вставку html-тегів в чатах, форумах і гостьових книгах – це красиво, але гра не варта свічок – навряд чи користувачі підуть до Вас на книгу або в чат, знаючи, що їх IP може стати відомим першому зустрічному хакеру. Та й не тільки IP – можливості javascript’a я перераховувати не буду 🙂

Для примітивної гостьової книги перерахованих засобів вистачить, щоб зробити її більш-менш складною для злому. Однак для зручності, книги зазвичай містять деякі можливості для модерування – як мінімум, можливість видалення повідомлень. Дозволену, природно, вузькому (або не дуже) колу осіб. Подивимося, що можна зробити тут.

Припустимо, вся система модерування книги також складається з двох частин – сторінки зі списком повідомлень, де можна відзначати підлягають видаленню повідомлення, і безпосередньо скрипта, що видаляє повідомлення. Назвемо їх відповідно admin1.php і admin2.php.

Найпростіший і надійний спосіб аутентикації користувача – розміщення скриптів в директорії, захищеної файлом. Htaccess. Для подолання такого захисту потрібно вже не додаток ламати, а web-сервер. Що дещо складніше і вже, в усякому разі, не укладається в рамки теми цієї статті. Проте не завжди цей спосіб придатний до вживання – іноді буває треба проводити авторизацію засобами самого додатка.

Перший, найпростіший спосіб – авторизація засобами HTTP – через код 401. При вигляді такого коду повернення, будь-яка нормальна браузер висвітить віконце авторизації та попросить ввести логін і пароль. А в подальшому браузер при одержанні коду 401 буде намагатися підсунути web-серверу поточні для даного realm’а логін і пароль, і тільки в разі невдачі зажадає повторної авторизації. Приклад коду для виводу вимоги на таку авторизацію є у всіх хрестоматіях і мануали:

if (!isset($PHP_AUTH_USER)) {
        Header("WWW-Authenticate: Basic realm=\"My Realm\"");
        Header("HTTP/1.0 401 Unauthorized");
        exit;
}

Розмістимо цей шматочок коду на початку скрипта admin1.php. Після його виконання, у нас будуть дві встановлені змінні $ PHP_AUTH_USER і PHP_AUTH_PW, в яких відповідно будуть лежати ім’я та пароль, введені користувачем. Їх можна, наприклад, перевірити по SQL-базі:

*** Увага! ***

У наведеному нижче фрагменті коду свідомо допущена серйозна помилка в безпеці. Спробуйте знайти її самостійно.

$sql_statement="select password from peoples where name='$PHP_AUTH_USER'";
$result = mysql($dbname, $sql_statement);
$rpassword = mysql_result($result,0,'password');
$sql_statement = "select password('$PHP_AUTH_PW')";
$result = mysql($dbname, $sql_statement);
$password = mysql_result($result,0);
if ($password != $rpassword) {
        Header("HTTP/1.0 401 Auth Required");
        Header("WWW-authenticate: basic realm=\"My Realm\"");
        exit;
}

Згадана помилка, між іншим, дуже поширена серед початківців та неуважних програмістів. Колись я сам піймався на цю вудку – на щастя, особливої ​​шкоди це не принесло, не вважаючи залишених хакером в новинній стрічці декількох нецензурних фраз.

Отже, розкриваю секрет: допустимо, хакер вводить свідомо неіснуюче ім’я користувача та порожній пароль. При цьому в результаті вибірки з бази змінна $ rpassword приймає пусте значення. А алгоритм шифрування паролів за допомогою функції СУБД MySQL Password (), так само, втім, як і стандартний алгоритм Unix, при спробі шифрування порожнього пароля повертає порожнє значення. В результаті – $ password == $ rpassword, умова виконується і зломщик отримує доступ до захищеної частини програми. Лікується це або забороною порожніх паролів, або, на мій погляд, більш правильний шлях – вставкою наступного фрагмента коду:

if (mysql_numrows($result) != 1) {
        Header("HTTP/1.0 401 Auth Required");
        Header("WWW-authenticate: basic realm=\"My Realm\"");
        exit;
}

Тобто – перевіркою наявності одного і тільки одного користувача в базі. Ні більше, ні менше.

Точно таку ж перевірку на авторизацію варто вмонтувати і в скрипт admin2.php. По ідеї, якщо користувач хороша людина – то він приходить до admin2.php через admin1.php, а значить, вже є авторизованим і ніяких повторних питань йому не буде – браузер нишком передасть пароль. Якщо ж ні – ну, тоді і посваритися не гріх. Скажімо, вивести ту ж фразу “hacker? He-he …”.

На жаль, не завжди вдається скористатися алгоритмом авторизації через код 401 і доводиться виконувати її тільки засобами додатку. У загальному випадку модель такої авторизації буде наступною:

Така модель називається сесійного – після проходження авторизації відкривається так звана “сесія”, протягом якої користувач має доступ до захищеної частини системи. Сесія закрилася – доступ закривається. На цьому принципі, зокрема, будується більшість www-чатів: користувач може отримати доступ до чату тільки після того, як пройде процедуру входу. Основна складність даної схеми полягає в тому, що всі скрипти захищеної частини аплікації якимось чином повинні знати про те, що користувач, який посилає дані, успішно авторизувався.

Розглянемо кілька варіантів, як це можна зробити:

  1. Після авторизації всі скрипти захищеної частини викликаються з якимось прапорцем виду adminmode = 1. (Не треба сміятися – я сам таке бачив).

    Ясно, що будь-який, кому відомий прапорець adminmode, може сам сформувати URL і зайти в режимі адміністрування. Крім того – немає можливості відрізнити одного користувача від іншого.

  2. Скрипт авторизації може яким-небудь чином передати ім’я користувача іншим скриптам. Поширена в багатьох www-чатах – для того, щоб відрізнити, де чиє повідомлення йде, поряд з формою типу text для введення повідомлення, пристроюється форма типу hidden, де вказується ім’я користувача. Теж ненадійно, тому що хакер може скачати документ із формою до себе на диск і поміняти значення форми hidden. Деяку користь тут може принести вищезгадана перевірка HTTP_REFERER – але, як я вже казав, ніяких гарантій вона не дає.

  3. Визначення користувача по IP-адресою. В цьому випадку, після проходження авторизації, де-небудь в локальній базі даних (sql, dbm, та хоч в txt-файлі) зберігається поточний IP користувача, а всі скрипти захищеної частини дивляться в змінну REMOTE_ADDR і перевіряють, чи є така адреса в базі. Якщо є – значить, авторизація була, якщо немає – “hacker? He-he …” 🙂

    Це більш надійний спосіб – не пройти авторизацію і отримати доступ вдасться лише в тому випадку, якщо з того ж IP сидить інший користувач, успішно авторизуватися. Проте, враховуючи поширеність проксі-серверів та IP-Masquerad’інга – це цілком реально.

  4. Єдиним, відомим мені простим і досить надійним способом верифікації особистості користувача є авторизація за допомогою random uid. Розглянемо її більш докладно.

Після авторизації користувача скрипт, який провів авторизацію, генерує досить довге випадкове число:

mt_srand((double)microtime()*1000000);
$uid=mt_rand(1,1000000);

Це число він:

а) заносить в локальний список авторизованих користувачів;
б) Видає користувачеві.

Користувач при кожному запиті, крім іншої інформації (повідомлення в чаті, або список повідомлень в гостьовій книзі), відправляє серверу свій uid. При цьому в документі з формами вводу буде присутнім, поряд з іншими формами, тег виду:

<input type=hidden name=uid value=1234567890>

Форма uid невидима для користувача, але вона передається скрипту захищеної частини програми. Той звіряє переданий йому uid з UID’ом, що зберігаються в локальній базі й або виконує свою функцію, або … “Hacker? he-he…”.

Єдине, що необхідно зробити при такій організації – періодично чистити локальний список uid’ов та / або зробити для користувача кнопку “вихід”, при натисканні на яку локальний uid користувача зітреться з бази на сервері – сесія закрита.

Деякі програмісти використовують як uid не “одноразове” динамічно генерує число, а пароль користувача. Це допустимо, але це є “поганим тоном”, оскільки пароль користувача звичайно не змінюється від сесії до сесії, а значить – хакер зможе сам відкривати сесії. Та ж сама модель може бути використана скрізь, де потрібна ідентифікація користувача – в чатах, веб-конференціях, електронних магазинах.

На закінчення варто згадати і про таку корисну річ, як ведення логів. Якщо в кожну з описаних процедур вбудувати можливість занесення події в лог-файл із зазначенням IP-адреси потенційного зловмисника – То в разі реальної атаки обчислити хакера буде набагато простіше, оскільки хакери зазвичай пробують послідовно ускладнюються атаки. Для визначення IP-адреси бажано використовувати не тільки стандартну змінну REMOTE_ADDR, але і менш відому HTTP_X_FORWARDED_FOR, яка дозволяє визначити IP користувача, що знаходиться за проксі-сервером. Природно – якщо проксі це дозволяє.

При веденні лог-файлів, необхідно пам’ятати, що доступ до них повинен бути тільки у Вас. Краще всього, якщо вони будуть розташовані за межами дерева каталогів, доступного через WWW. Якщо немає такої можливості – Створіть окремий каталог для лог-файлів і закрийте туди доступ за допомогою. Htaccess (Deny from all).

Я буду дуже вдячний, якщо хто-небудь з програмістів поділиться своїми не описаними тут методами забезпечення безпеки при розробці додатків для Web.

P.S. Висловлюю глибоку подяку Козіну Максиму (madmax@express.ru) за рецензування даної статті і ряд дуже цінних доповнень.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*