Після більш ніж місяця тиші маю честь представити вам чергову статтю про веб-безпеку.
Міжсайтовий скриптинг (XSS) - це найчастіше використовуваний тип атак на веб-сайти (нещодавню статистику можна побачити у першій замітці із серії про безпеку у PHP). Для такої атаки, зловмисник зберігає у базі даних сайта, спеціально створений CSS, HTML або JavaScript. Пізніше, коли цей контент виводиться користувачу, скажімо у якості коментаря у блозі чи повідомлення на форумі, він змінює відображення сторінки чи виконує код, мета якого - вкрасти дані користувача, передати конфіденційні дані на лівий сервер чи якимось чином змінити функціонування сайта.
XSS - дуже популярний вид атаки та його часто дуже легко застосувати, адже неймовірно велика кількість сайтів просто виводить ввід користувача, без будь-якої попередньої фільтрації. Фактично, вдала XSS атака є результатом неякісного коду програми.
Два основних види XSS атак це:
- Пряма дія - вставлений контент відображується тому користувачу, який його власне вставив;
- Збережена дія - будь-яка кількість користувачів побачить (і, можливо, відчує) вставлений контент.
Метою прямої дії зазвичай є вивчення методів фільтрації вводу програмою, якщо такий взагалі є для того, щоб сконструювати більш значиму атаку. Збережена ж дія є найбільш небезпечним типом XSS, адже у результаті можуть бути вкрадені конфіденційні дані користувачів чи порушене функціонування самого сайту.
У цій частині ми розглянемо деякі методи боротьби із XSS (інші будуть розглянуті у наступних частинах).
Закодувати все!
Отже, як же нам убезпечити увесь ввід від XSS? На щастя, PHP має вдосталь вбудованих функцій, які видаляють або кодують спеціальні HTML символи.
Перша з них - htmlspecialchars. Вона приймає строку із введеними даними та кодує символи & (амперсанд), < (менш ніж), > (більш ніж), ” (подвійні лапки) та (необов’язково) ‘ (одинарні лапки). Всі вони перетворюються у відповідні HTML-сутності такі, як < (<), & (&) і т.п. Це змушує браузер трактувати ці символи виключно як літеральні (тобто такі, які не мають ніякого спеціального змісту).
<?php $input = '<a href="very.bad.com"><img src="click.gif" /></a>'; $encoded = htmlspecialchars($input); //<a href="http://very.bad.com"><img src="click.gif" /></a> ?>
Тож краще кодувати навіть найпростіший ввід.
Вгамування атрибутів
Якщо необхідність кодування теґів HTML очевидна, то не всі усвідомлюють цю необхідність і для атрибутів теґів.
Значна кількість користувацького вводу врешті-решт опиняється у атрибутах теґів, які можуть надавати елементу стиль чи навіть виконувати JavaScript. Для ясності розглянемо приклад. Користувач відправляє URL, щоб вказати на цікаву сторінку. Цей ввід використовується для того, щоб сформувати теґ <a>: <a href=”http://webdeveloping.com.ua”>Блоґ про веб-розробку українською</a>. Наче нормальна ситуація, а тепер уявімо, що користувач вставить у свій ввід лапки. І як тільки закриваючі лапки будуть знайдені, браузер закриє цей атрибут і відкриє новий. І тут зловмисник може зробити все, що завгодно.
Також слід пам’ятати, що типово одинарні лапки не будуть закодовані, а тільки подвійні. Тож, якщо ви використовуєте одинарні лапки у вашому HTML, це треба вказати функції htmlspecialchars другим параметром таким чином: htmlspecialchars(”‘”, ENT_QUOTES). Інакше можливий такий сценарій:
<?php $input = htmlspecialchars("#' real_url='http://mailicious.com' fake_url='http://php.net' onmouseover='window.status=this.attributes.fake_url.value; return true' onclick='window.location=this.attributes.real_url.value'"); echo "<a href='$input'">Корисне посилання</a>; ?>
Отже, що робить цей ввід? А він користується саме тим, що htmlspecialchars типово не кодує одинарні лапки, а сторінка їх використовує. Вводячи два атрибути real_url та fake_url, які є відповідно справжньою адресою та підробленою, цей код вставляє підроблену у строку статусу браузера при наведенні на посилання, та переходить на справжню при натисканні. Таким чином наш користувач навіть не помітить, що його хочуть відправити кудись не туди.
HTML-сутності та фільтри
Символ амперсанда часто використовується як початок HTML-сутності, але також може бути задіяним для обходу різних фільтрів. Нехай у нас існує фільтр, що знаходить всі строки “PERL” та видаляє їх. Користувач може легко закодувати всі символи цієї строки у їх відповідні HTML-сутності, таким чином обходячи фільтр стороною:
<?php $input = 'PERL'; // PERL в закодованому вигляді echo str_ireplace('perl', '', $input); // Виведе незмінену строку, адже str_ireplace не знайде того, що треба ?>
Отже, перед фільтром нам треба закодувати ввід, щоб розбити сутності. Всі амперсанди будуть закодовані у &, тож спеціально створений ввід не пройде у запланованому вигляді:
<?php $input = 'PERL'; // PERL в закодованому вигляді $input = htmlspecialchars($input); // &#80;&#69;&#82;&#76; echo str_ireplace('perl', '', $input); //Все так само нічого не відфільтрує // Тепер браузер виведе PERL ?>
Це вже краще, але насправді, кодування амперсандів - це не завжди добре та в деяких випадках може навіть зашкодити. Це актуально наприклад для застарілих сайтів, які не використовують Unicode. Уявімо таку ситуацію (поширену на сайтах, що не думають про інтернаціоналізацію): є сторінка із формою у кодуванні ISO-8859-1 (типова латинська), а користувач вводить дані у кодуванні CP-1251 (типова кирилічна). Браузер автоматично переведе всі символи із вводу у HTML-сутності задля їх правильного виводу. Ми ж на стороні сервера кодуємо всі амперсанди із вводу, які в свою чергу втрачають початковий зміст, та в результаті отримуємо нечитабельний варіант.
Тож як нам передбачити цю ситуацію? Правильніше за все - використовувати Unicode
Але, якщо це з деяких причин неможливо - найліпшим варіантом є задіяти регулярний вираз, щоб перевести у дійсні сутності лише подвійно-закодовані символи:
<?php preg_replace('!&#([0-9]+);!', '&#\1;', htmlspecialchars($input)); ?>
Цим виразом ми захоплюємо подвійно-закодовані літери (адже шукаємо лише чисельні значення після &#) та замінюємо їх на дійсні сутності. Ми вирішуємо проблему із подвійно-закодованими літерами, але знову повертаємо попередню проблему. В такому випадку спеціально закодоване користувачем “PERL” буде знову виведене браузером, оминаючи фільтр.
Отже, нам потрібна краща логіка обробки HTML-сутностей. Для цього існує функція preg_replace_callback - вона задіює, передану їй функцію до кожного знайденого набору, що задовольняє регулярному виразу. Цій функції передається один масив, першим елементом якого є набір, що задовольняє виразу, а всіма наступними є набори, які задовольняють всім під-виразам. Тепер у нас є все, щоб дещо покращити наш фільтр:
<?php $input = htmlspecialchars('PERL'); function decode($matches) { if ($matches[1] > 255) { // не-ASCII return '&#'.$matches[1].';'; // переводимо у дійсну сутність } if (($matches[1] >= 65 && $matches[1] <= 90) || // A - Z ($matches[1] >= 97 && $matches[1] <= 122) || // a - z ($matches[1] >= 48 && $matches[1] <= 57)) { // 0 - 9 return chr($matches[1]); // переводимо у літеральну форму } return $matches[0]; // все інше залишаємо без змін } echo preg_replace_callback('!&#([0-9]+);!', 'decode', $input); // PERL ?>
У цьому прикладі функцію decode викликає preg_replace_callback та використовує під-вираз, який має чисельне значення для порівняння. Якщо це значення більше за 255 (тобто виходить за межі таблиці ASCII), то воно має бути перетворене у дійсну сутність, змінюючи & на &. Для значень іншого діапазону ми робимо додаткову перевірку. Якщо це спеціальний символ (такий як ‘ чи <), то залишаємо все без змін, інакше (цифра або латинська літера) - переводимо у звичайну літеральну форму.
Однак навіть цього недостатньо, адже залишається декілька моментів із HTML-сутностями. Наприклад, сутність не потребує точки із комою в кінці. Тож - - цілком дійсна сутність, яку браузери чудово зрозуміють. Але в такому випадку наші регулярні вирази проваляться та нічого не знайдуть. Більше того, числове значення сутності може бути представлене у вигляді шістнадцяткового числа, тож Z - теж цілком дійсна сутність. А наші попередні регулярні вирази це також не зрозуміють. Отже, вдосконалюємо далі:
<?php preg_replace_callback('!&#((?:[0-9]+)|(?:x(?:[0-9A-F]+)));?!i', 'decode', $input); ?>
Трошки ускладнили наш вираз. Тепер з одного під-виразу утворилось двоє: перший спрацьовує, коли ми маємо одну або більше цифру, другий - якщо є символ “x”, за яким йде одна або більше цифра чи літера латинського алфавіту з діапазону [A-F]. Окрім того, ми більше не вимагаємо символ “;” в кінці. Тут слід зазначити, що квантор “?:” означає те, що ми не зберігаємо результат під-запиту, таким чином у результуючому масиві буде так само два елементи. Модифікатор “i” в кінці виразу повідомляє про те, що порівняння відбувається незалежно від регістру символів.
Тепер розглянемо відповідні доповнення до функції розкодування:
<?php function decode($matches) { if (!is_int($matches[1]{0})) { $val = '0'.$matches[1] + 0; } else { $val = (int) $matches[1]; } if ($val > 255) { return '&#'.$matches[1].';'; } if (($val >= 65 && $val <= 90) || ($val >= 97 && $val <= 122) || ($val >= 48 && $val <= 57)) { return chr($val); } return $matches[0]; } ?>
Тут ми додали перевірку і приведення для шістнадцяткових чисел. Спочатку перевіряємо чи перший символ результату є числом. Якщо це так, то маємо десяткове число, приводимо його до цілочислового типу (int), інакше (перший символ - “x”) - дописуємо 0 спочатку (таким чином PHP починає розуміти, що йому згодували шістнадцяткове число, тобто маємо щось типу 0×5F) та неявно переводимо до десяткового формату, додаючи до нашого числа 0. Далі код не змінився.
Ось такий буде результат:
<?php $input = 'PERL<Aяド'; // результат обробки: // PERL&#60;Aяド ?>
Ось тепер все добре, і після цієї обробки нарешті можна використовувати літеральну фільтрацію
To be continued…

















