Прийоми безпечного програмування веб-додатків на
PHP h2>
Дана
стаття не претендує на роль всеосяжного керівництва на тему "як
зробити так, щоб мене ніхто не поламав ". Так не буває. Єдина мета
цієї статті - показати деякі використовувані мною прийоми для захисту веб -
додатків типу WWW-чатів, гостьових книг, веб-форумів та інших програм
подібного роду. Отже, давайте розглянемо деякі прийоми програмування на
прикладі певної гостьової книги, написаної на PHP. p>
Першої
заповіддю веб-програміста, що бажає написати більш-менш захищене
веб-додаток, має стати "Ніколи не вір даними, що надсилаються тобі
користувачем ". Користувачі - це за визначенням такі злісні хакери,
які тільки й шукають моменту, як би напхати у форми введення всякую дрянь типу
PHP, JavaScript, SSI, викликів своїх моторошно хакерських скриптів і тому подібних
жахливих речей. Тому перше, що необхідно зробити - це жорстоким чином
відфільтрувати всі дані, надіслані користувачем. p>
Припустимо,
у нас в гостьовій книзі існує 3 форми введення: ім'я користувача, його e-mail і
саме по собі тіло повідомлення. Перш за все, обмежимо кількість даних,
переданих з форм введення чим-небудь на кшталт: p>
p>
На
роль цієї захисту, звичайно, це претендувати не може - єдине
призначення цього елементу - обмежити користувача від випадкового введення імені
довше 20-ти символів. А для того, щоб у користувача не виникло спокуси
завантажити документ з формами введення і підправити параметр maxlength, встановимо
де-небудь на самому початку скрипта, що обробляє дані, перевірку змінної
оточення web-сервера HTTP-REFERER: p>
p>
Тепер,
якщо дані передані не з форм документа, що знаходиться на сервері
www.myserver.com, хацкеру буде видано деморалізуючий повідомлення. Насправді,
і це теж не може служити 100%-ою гарантією того, що дані ДЕЙСТВИТЕЛЬНО
передані з нашого документа. Врешті-решт, мінлива HTTP_REFERER
формується браузером, і ніхто не може перешкодити хакеру підправити код
браузера, або просто зайти Телнет на 80-ый порт і сформувати свій запит.
Так що подібний захист годиться тільки від Ну Зовсім неосвічених хакерів.
Втім, за моїми спостереженнями, близько 80% відсотків зловмисників на цьому етапі
зупиняються і далі не лізуть - чи то IQ не дозволяє, чи то просто лінь.
Особисто я просто виніс цей фрагмент коду в окремий файл, і викликаю його
звідусіль, звідки це можливо. Часу на звернення до змінної йде трохи
- А береженого Бог береже. P>
Наступним
етапом стане горезвісна жорстка фільтрація переданих даних. Перш за все,
не будемо довіряти змінної maxlength у формах введення і ручками поріжемо рядок: p>
$ username = substr ($ username, 0,20); p>
Не
дамо користувачеві використовувати порожнє поле імені - просто так, щоб не давати
писати анонімні повідомлення: p>
if (empty ($ username)) ( p>
echo "invalid username"; p>
exit; p>
) p>
Заборонимо
користувачеві використовувати в своєму імені будь-які символи, крім літер російського і
латинського алфавіту, знака "_" (почерк), пробілу та цифр: p>
if
(preg_match ("/[^( w) | (x7F-xFF) | (s )]/",$ username)) ( p>
echo "invalid username"; p>
exit; p>
) p>
Я
волію скрізь, де потрібно що-небудь більш складне, ніж перевірити наявність
патерну в рядку або поміняти один патерн на інший, використовувати
Перл-сумісні регулярні вирази (Perl-compatible Regular Expressions). Те
ж саме можна робити і використовуючи стандартні PHP-шні ereg () і eregi (). Я не
буду наводити тут ці приклади - це досить докладно описано в мануалі. p>
Для
поля введення адреси e-mail додамо в список дозволених символів знаки
"@" І ".", Інакше користувач не зможе коректно ввести
адресу. Зате приберемо російські букви і пробіл: p>
if
(preg_match ("/[^( w )|(@)|(.)]/",$ usermail)) ( p>
echo
"invalid mail"; p>
exit; p>
) p>
Поле
введення тексту ми не будемо піддавати таким жорстким репресіям - перебирати всі
розділові знаки, які можна використовувати, просто лінь, тому
обмежимося використанням функцій nl2br () і htmlspecialchars () - це не дасть
ворогові понатикати в текст повідомлення html-тегів. Деякі розробники,
напевно, скажуть: "а ми все-таки дуже хочемо, щоб користувачі _моглі_
вставляти теги ". Якщо дуже кортить - можна зробити якісь
тегозаменітелі, типу "текст, оточений зірочками, буде висвітлений
bold'ом. ". Але ніколи не слід дозволяти користувачам використання
тегів, які мають на увазі підключення зовнішніх ресурсів - від тривіального
до супернавороченого. p>
Якось
раз мене попросили потестувати html-чат. Першим же поміченим мною багом було
саме дозвіл вставки картинок. З огляду на ще кілька особливостей будови
чату, через кілька хвилин у мене був файл, в якому акуратно були
перераховані IP-адреси, імена та паролі всіх присутніх у цей момент на
чаті користувачів. Як? Та дуже просто - чату був посланий тег, в результаті чого браузери всіх
користувачів, які були присутні в той момент на чаті, викликали скрипт
myscript.pl з хоста myserver.com. (там не було людей, що сиділи під lynx'ом :-)
). А скрипт, перед тим як видати location на картинку, звалив мені в лог-файл
половину змінних оточення - зокрема QUERY_STRING, REMOTE_ADDR та інших.
Для кожного користувача. З вищезазначеним результатом. P>
Тому
моя думка - так, дозволити вставку html-тегів в чатах, форумах і гостьових
книгах - це красиво, але гра не варта свічок - навряд чи користувачі підуть до Вас
на книгу або в чат, знаючи, що їх IP може стати відомим першому зустрічному
хакеру. Та й не тільки IP - можливості javascript'a я перераховувати не буду :-) p>
Для
примітивної гостьової книги перерахованих коштів вистачить, щоб зробити її
більш-менш складною для злому. Однак для зручності, книги звичайно містять
деякі можливості для модерування - як мінімум, можливість видалення
повідомлень. Дозвіл, природно, вузькому (або не дуже) колу осіб.
Подивимося, що можна зробити тут. P>
Припустимо,
вся система модерування книги також складається з двох частин - сторінки з
списком повідомлень, де можна відзначати що підлягають видаленню повідомлення, і
безпосередньо скрипта, що видаляє повідомлення. Назвемо їх відповідно
admin1.php і admin2.php. p>
Найпростіший
і дуже надійний спосіб аутентікаціі користувача - розміщення скриптів в
директорії, захищеної файлом. htaccess. Для подолання такого захисту потрібно вже
не додаток ламати, а web-сервер. Що дещо складніше і вже, в усякому
випадку, не вкладається в рамки теми цієї статті. Однак не завжди цей спосіб
придатний до вживання - іноді буває треба проводити авторизацію засобами
самого додатка. p>
Перший,
найпростіший спосіб - авторизація засобами HTTP - через код 401. При вигляді
такого коду повернення, будь-який нормальний браузер висвітить віконце авторизації та
попросить ввести логін і пароль. А надалі браузер при одержанні коду 401
буде намагатися підсунути web-серверу поточні для даного realm'а логін і
пароль, і тільки в разі невдачі потребують повторної авторизації. Приклад коду
для виведення вимоги на таку авторизацію є у всіх хрестоматіях і
мануали: p>
if (! isset ($ PHP_AUTH_USER)) ( p>
Header ( "WWW-Authenticate: Basic
realm = "My Realm ""); p>
Header ( "HTTP/1.0 401 Unauthorized "); p>
exit; p>
) p>
Розмістимо
цей шматочок коду на початку скрипта admin1.php. Після його виконання, у нас
будуть дві встановлені змінні $ PHP_AUTH_USER і PHP_AUTH_PW, в яких
відповідно будуть лежати ім'я та пароль, введені користувачем. Їх можна, до
Наприклад, перевірити по SQL-базі: p>
***
Увага !!!*** p>
В
наведеному нижче фрагменті коду свідомо допущена серйозна помилка в
безпеки. Спробуйте знайти її
самостійно. p>
$ sql_statement = "select password
from peoples where name = '$ PHP_AUTH_USER'"; p>
$ result = mysql ($ dbname,
$ sql_statement); p>
$ rpassword = mysql_result ($ result, 0, 'password'); p>
$ sql_statement = "select
password ( '$ PHP_AUTH_PW')"; p>
$ result = mysql ($ dbname,
$ sql_statement); p>
$ password = mysql_result ($ result, 0); p>
if ($ password! = $ rpassword) ( p>
Header ( "HTTP/1.0 401 Auth
Required "); p>
Header ( "WWW-authenticate: basic
realm = "My Realm ""); p>
exit; p>
) p>
Згадана
помилка, між іншим, дуже поширена серед початківців та неуважних
програмістів. Колись я сам впіймався на цю вудку - на щастя, особливого
шкоди це не принесло, не рахуючи надісланих хакером у стрічці новин
декількох нецензурних фраз. p>
Отже,
розкриваю секрет: припустимо, хакер вводить свідомо неіснуюче ім'я
користувача і порожній пароль. При цьому в результаті вибірки з бази мінлива
$ rpassword приймає пусте значення. А алгоритм шифрування паролів за допомогою
функції СУБД MySQL Password (), так само, втім, як і стандартний алгоритм
Unix, при спробі шифрування порожнього пароля повертає порожнє значення. У підсумку
- $ Password == $ rpassword, умова виконується і зломщик отримує доступ до
захищеної частини програми. Лікується це або забороною пустих паролів, або,
на мій погляд, більш правильний шлях - вставкою наступного фрагмента коду: p>
if (mysql_numrows ($ result)! = 1) ( p>
Header ( "HTTP/1.0 401 Auth
Required "); p>
Header ( "WWW-authenticate: basic
realm = "My Realm ""); p>
exit; p>
) p>
Те
є - перевіркою наявності одного і тільки одного користувача в базі. Ні більше,
ні менше. p>
Точно
таку ж перевірку на авторизацію варто вбудувати і в скрипт admin2.php. По ідеї,
якщо користувач хороша людина - то він приходить до admin2.php через
admin1.php, а отже, вже є авторизованим і ніяких повторних питань
йому не буде - браузер нишком передасть пароль. Якщо ж ні - ну, тоді й
посваритися не гріх. Скажімо, вивести ту ж фразу "hacker? He-he ...". p>
До
жаль, не завжди вдається скористатися алгоритмом авторизації через код
401 і доводиться виконувати її тільки засобами програми. У загальному випадку
модель такої авторизації буде наступною: p>
Користувач
один раз авторизуйтеся за допомогою веб-форми і програми, яку перевіряє
правильність імені та пароля. p>
Решта
скрипти захищеної частини програми яким-небудь чином перевіряють факт
авторизованого користувача. p>
Така
модель називається сесійного - після проходження авторизації відкривається так
звана "сесія", протягом якої користувач має доступ до
захищеної частини системи. Сесія закрилася - доступ закривається. На цьому
принципі, зокрема, будується більшість www-чатів: користувач може
отримати доступ до чату тільки після того, як пройде процедуру входу. Основна
складність даної схеми полягає в тому, що всі скрипти захищеної частини
програми якимось чином повинні знати про те, що користувач, який посилає
дані, успішно авторизуватися. p>
Розглянемо
кілька варіантів, як це можна зробити: p>
Після
авторизації всі скрипти захищеної частини викликаються з якимось прапорцем виду
adminmode = 1. (Не треба сміятися - я сам таке бачив). P>
Ясно,
що будь-хто, кому відомий прапорець adminmode, може сам сформувати URL і зайти в
режимі адміністрування. Крім того - немає можливості відрізнити одного
користувача від іншого. p>
Скрипт
авторизації може яким-небудь чином передати ім'я користувача іншим
скриптам. Поширена в багатьох www-чатах - для того, щоб відрізнити, де
чиє повідомлення йде, поряд з формою типу text для введення повідомлення,
пристроюється форма типу hidden, де вказується ім'я користувача. Теж
ненадійно, тому що хакер може завантажити документ з формою до себе на диск і
поміняти значення форми hidden. Деяку користь тут може принести
вищезгадана перевірка HTTP_REFERER - але, як я вже говорив, жодних гарантій
вона не дає. p>
Визначення
користувача по IP-адресою. У цьому випадку, після проходження авторизації,
де-небудь в локальній базі даних (sql, dbm, та хоч в txt-файлі) зберігається
поточний IP користувача, а всі скрипти захищеної частини дивляться в змінну
REMOTE_ADDR і перевіряють, чи є такий адреса в базі. Якщо є - значить,
авторизація була, якщо ні - "hacker? he-he ..." :-) p>
Це
більш надійний спосіб - не пройти авторизацію і отримати доступ вдасться лише в
тому випадку, якщо з того ж IP сидить інший користувач, успішно
авторизуватися. Однак, з огляду на поширеність проксі-серверів і
IP-Masquerad'інга - це цілком реально. P>
Єдиним,
відомим мені простим і досить надійним способом верифікації особистості
користувача є авторизація за допомогою random uid. Розглянемо її більш
докладно. p>
Після
авторизації користувача скрипт, який провів авторизацію, генерує достатньо
довге випадкове число: p>
mt_srand ((double) microtime () * 1000000); p>
$ uid = mt_rand (1,1000000); p>
Це
число він: p>
а)
заносить в локальний список авторизованих користувачів; p>
б)
Видає користувачеві. P>
Користувач
при кожному запиті, крім іншої інформації (повідомлення в чаті, або список повідомлень
в гостьовій книзі), відправляє серверу свій uid. При цьому в документі з формами
введення буде присутній, поряд з іншими формами, тег виду: p>
p>
Форма
uid невидима для користувача, але вона передається скрипту захищеної частини
додатки. Той звіряє переданий йому uid з uid'ом, що зберігаються в локальній
базі і або виконує свою функцію, або ... "hacker? he-he ...". p>
Єдине,
що необхідно зробити за такої організації - періодично чистити локальний
список uid'ов та/або зробити для користувача кнопку "вихід", при
натисканні на яку локальний uid користувача зітреться з бази на сервері --
сесія закрита. p>
Деякі
програмісти використовують як uid не "одноразове" динамічно
генерований число, а пароль користувача. Це допустимо, але це є
"поганим тоном", оскільки пароль користувача звичайно не змінюється від
сесії до сесії, а значить - хакер зможе сам відкривати сесії. Та ж сама
модель може бути використана скрізь, де потрібна ідентифікація користувача
- В чатах, веб-конференціях, електронних магазинах. P>
В
закінчення варто згадати і про таку корисної речі, як ведення логів. Якщо в
кожну з описаних процедур вбудувати можливість занесення події в лог-файл
із зазначенням IP-адреси потенційного зловмисника - то в разі реальної
атаки обчислити хакера буде набагато простіше, оскільки хакери зазвичай пробують
послідовно ускладнюються атаки. Для визначення IP-адреси бажано
використовувати не тільки стандартну змінну REMOTE_ADDR, але і менш відому
HTTP_X_FORWARDED_FOR, яка дозволяє визначити IP користувача,
що знаходиться за проксі-сервером. Природно - якщо проксі це дозволяє. P>
При
ведення лог-файлів, необхідно пам'ятати, що доступ до них повинен бути тільки у
Вас. Найкраще, якщо вони будуть розташовані за межами дерева каталогів,
доступного через WWW. Якщо немає такої можливості - створіть окремий каталог
для лог-файлів і закрийте туди доступ за допомогою. htaccess (Deny from all). p>
Список літератури h2>
Для
підготовки даної роботи були використані матеріали з сайту http://www.hostmake.ru/
p>