XSS — это тип уязвимости, когда у злоумышленника получается пропихнуть вредоносный HTML- или JS- код в страницу на сайте. Напомню, что JS (яваскрипт) — это язык, программы (скрипты) на котором можно встраивать в HTML-страницу и выполнять внутри браузера. Если злодею это удается сделать, то при открытии такой страницы в браузере внедренный JS-код может выполнять любые действия на сайте от имени пользователя, например, воровать его куки (чтобы злоумышленник потом с их помощью зашел на сайт), рассылать спам, копировать личные данные пользователя, перенаправлять пользователей на другой сайт, нажимать кнопки, отправлять сообщения - в общем, такой код может сделать много нехороших вещей.
Самый простой пример уязвимости получается, когда переданные через GET-параметр значения выводятся (добавляются в код страницы) как есть, например, http://example.com?q=[... html и js код ...]. Но вообще, XSS атаку можно провести и через любые другие данные, например, POST-, куки, HTTP-заголовки, данные из базы (которые хакер записал туда раньше). То есть на более-менее сложном сайте невозможно отфильтровать все входные данные, это бесперспективный путь.
Также, пытаться фильтровать все GET/POST данные (как рекомендуют некоторые учебники) не очень перспективно, так как есть очень много способов подсунуть злой код (примеры: https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet ). Трудно написать фильтр, который сможет ловить все эти коды.
Прежде чем говорить о методах борьбы, рассмотрим 2 простых примера. Допустим, школьник Вася сделал на своем сайте скрипт search.php
для поиска информации на сайте. Данные для поиска передаются через GET-параметр и выводятся без фильтрации. Вот, как выглядит код:
// ....
$query = $_GET['q'];
echo "<p>Вы искали: $query</p>";
// .... дальше идет код вывода результатов поиска ...
Если посмотреть повнимательнее, то видно, что данные, переданные в URL (и попавшие в $_GET
), выводятся на странице без всяких преобразований.
Злоумышленник может сделать ссылку:
http://example.com/search.php?q=<script>зловредный скрипт, ворующий все куки пользователя</script>
Содержимое параметра q без изменений вставится в страницу и получится код:
<p>Вы искали: <script>зловредный скрипт, ворующий все куки пользователя</script>
Код на яваскрипте, написанный внутри тега <script>
, выполняется браузером при отображении страницы. Следовательно, злоумышленник может создать ссылку, при открытии которой пользователем выполнится любой заложенный в этой ссылке JS-код.
Злоумышленник посылает ссылку Васе (автору сайта), тот ее открывает в браузере, запускается скрипт и ворует куки Васи. Злоумышленник выкладывает куки на анонимном форуме и все его посетители, с помощью кук зайдя на сайт под администратором, дружно издеваются над сайтом Васи.
Наученный горьким опытом, Вася почитал устаревшие учебники по PHP4 и увидел там совет "очищать" пришедшие от пользователя данные. Вася поставил фильтр, не пропускающий угловые скобки и слово «script». И переделал свой код. Теперь данные фильтруются и Вася спит спокойно (однако, поиск по ключевому слову script
теперь не работает):
....
$query = $_GET['q'];
$query = preg_replace("/<|>|script/ui", '', $query);
echo "<p>Вы искали: $query</p>";
// .... дальше идет код вывода результатов поиска ...
echo "Искать еще: <input type=text value='$query'><button type=button>Искать</button>";
// .....
Хакер так просто не сдается. Он делает ссылку вида
/search.php?q=' autofocus onfocus='злой код'
и снова отправляет ее админу Васе. Админ открывает страницу, и запускается злой скрипт. Почему? Посмотрим, что получилось при подстановке параметра q
в input type=text
:
Искать еще: <input type=text value='' autofocus onfocus='злой код'
'><button type=button>Искать</button>
Как видим, за счет кавычки текст злоумышленника смог "выйти" за пределы атрибута value
и добавить дополнительные атрибуты к тегу <input>
. Атрибут autofocus
вызывает автоматическую установку курсора в поле при загрузке страницы, а атрибут onfocus
содержит JS-скрипт, который срабатывает при установке курсора в поле (в HTML много таких атрибутов, они все начинаются с on...
). Вася опять попался (и в этот раз хакер отыгрался на его сайте по полной программе).
Если читатель не хочет повторять ошибки Васи, ему стоит выбросить устаревшие учебники и читать дальше.
Средство борьбы одно: все данные, выводимые из переменных (неважно, откуда они пришли), в HTML код, надо пропускать через функцию htmlspecialchars
(мануал по ней: http://php.net/manual/ru/function.htmlspecialchars.php ):
echo "<p>Вы искали: ".htmlspecialchars($query, ENT_QUOTES)."</p>";
echo "Искать еще: <input type=text value='".htmlspecialchars($query)."'><button type=button>Искать</button>";
Так надо делать со всеми переменными без исключения, значения которых выводятся на странице. Эта функция, htmlspecialchars
, заменяет символы <
, >
, "
, '
, &
на HTML-мнемоники так, что любые теги и кавычки не ломают HTML-код, а просто выводятся как текст.
Символ | Заменяется на |
---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
' |
Теперь хакер может передавать любые спецсимволы в параметре q
и уязвимости не будет, так как код <script>
заменится на конструкцию <script>
, которая воспринимается браузером как текст <script>
(не как HTML-тег, а как просто текст, состоящий из угловых скобок и слова script).
Можете сделать этот скрипт, и проверить сами.
Если лень писать каждый раз htmlspecialchars($query, ENT_QUOTES)
, можно сделать функцию с коротким именем:
function html($text) {
return htmlspecialchars($text, ENT_QUOTES);
}
Обратите внимание на параметр ENT_QUOTES
— если его забыть указать, то одиночная кавычка не экранируется.
Хорошие шаблонизаторы вроде Twig экранируют данные при выводе автоматически.
Если на сайте пользователь может вводить и сохранять ссылки (например, в профиле в разделе «О себе»), то надо проверять, что они начинаются с http:// или https:// . Иначе пользователь подсунет ссылку вида data://... или javascript:// ...., при клике по которой выполнится заложенный в ней JS-код. Конечно, ему надо еще заманить других пользователей кликнуть по ней, но наверняка есть способы для этого. htmlspecialchars
тут не спасет, но поможет определение протокола ссылки с помощью parse_url()
или проверка ссылки регулярным выражением.
Дополнительная защита никогда не помешает, верно? У кук уже несколько лет есть специальный дополнительный параметр: httpOnly
. Если его указать при создании куки, то такая кука будет недоступна яваскрипту на странице и злоумышленник, даже найдя XSS, не сможет её украсть (но другие нехорошие вещи он все равно сможет сделать, так что это не дает 100% защиту, а лишь усложняет атаку).
Вот, как создать http-only куку в PHP:
setcookie ('auth', '12344567890', 0, '/', null, false, true)
последний параметр, true
, включает опцию http-only. Думаю, стоит включать эту полезную опцию для всех кук, которые как-то авторизуют пользователя. Мануал: http://php.net/manual/en/function.setcookie.php
В некоторых браузерах (Chrome например) есть простая защита от XSS (XSS Auditor). Chrome при загрузке страницы проверяет все находящиеся в ней скрипты и сравнивает с URL. Если JS-код совпадает с строкой из URL, то он вырезается и не выполняется (эта защита сработала бы в первом примере, где хакер внедрил тег <script>
с кодом на страницу через параметр в строке запроса). Однако эта система защищает не от 100% атак, а только от части случаев и иногда обходится. Потому ты не должен на нее полагаться. Информация: http://habrahabr.ru/post/143022/ (хабр)
Новые браузеры поддерживают специальный заголовок Content-Security-Policy
, который определяет какие скрипты (а также CSS файлы и файлы плагинов вроде флеш-роликов) и из каких источников можно подключать на странице. Если убрать весь JS-код в внешние скрипты, размещенные на сервере, и запретить в этом заголовке выполнять скрипты с чужих сайтов и unsafe-inline
(скрипты, вписанные в страницу, а также атрибуты-обработчики вроде onclick
), то вы сможете добавить дополнительную защиту своему сайту от большинства XSS. Также, браузер будет отправлять отчеты в формате JSON о попытках нарушить эти ограничения. Вот статьи, но учтите, что они могут быть сложными для начинающего:
- http://habrahabr.ru/company/yandex/blog/206508/
- https://xakep.ru/2013/12/23/61798/
- https://developer.mozilla.org/ru/docs/Web/Security/CSP
Иногда есть ситуации, когда надо разрешить пользователю выводить на сайте HTML-код. Ну например, если мы разрабатываем блог и хотим, чтобы пользователи могли публиковать текст с заголовками, картинками, ссылками, то есть со сложным форматированием. htmlspecialchars
тут не поможет, так как она просто превратит все теги в обычный текст.
Тут есть 2 пути решения:
1. Придумать какую-то разметку, которая позже преобразуется в HTML-код. Пример такой разметки - Markdown. Для неё написаны библиотеки, преобразующие текст с разметкой в HTML-код. Например, текст вида
Hello, **world**
Превращается в HTML-код
Hello, <strong>world</strong>
Минусы этого подхода:
- пользователям надо изучить язык разметки, нельзя просто использовать WYSIWYG-редактор
- нужно тщательно проверить библиотеку конвертации Markdown в HTML, что в ней нет никаких способов ввести потенциально опасные HTML-теги вроде
<script>
. Зачастую такая возможность есть.
2. Использовать фильтр HTML-кода, который пропускает только теги и атрибуты из разрешенного белого списка, а также проверяет все ссылки (если они разрешены) на то, что в них используются разрешенные протоколы (чтобы, например ссылка вида data://... не прошла).
Есть много готовых библиотек с такими возможностями, но нужно внимательно смотреть на принцип их работы. Если библиотека использует регулярные выражения для фильтрации кода - с большой вероятностью, найдется конструкция, с помощью которой ее можно обойти, так как регулярными выражениями почти невозможно разобрать все аспекты HTML-кода. Правильно написанная библиотека разбирает HTML-код, преобразует его в дерево DOM, для работы с которым в PHP есть расширение DOM. Затем библиотека обходит полученное дерево, проверяя каждый тег, атрибут и ссылку на соответствие белому списку и удаляет те, что не разрешены. Затем дерево преобразуется обратно в HTML-код.
При использовании фильтра мы можем дать пользователю стандартный WYSIWYG-редактор (список таких редакторов: https://github.com/cheeaun/mooeditable/wiki/Javascript-WYSIWYG-editors (англ.)). Редактор отправляет текст с форматированием на сервер в виде HTML-кода, который на сервере очищается с помощью библиотеки-фильтра.
Пример хорошей библиотеки-фильтра, использующей DOM: http://htmlpurifier.org/ (англ.)
- Выводим данные только через
htmlspecialchars
- Проверяем протокол в ссылках, пришедших от пользователей
- Используем httpOnly куки
- http://html5sec.org/ (содержит примеры различных способов выполнить скрипт на странице)
- http://habrahabr.ru/post/143259/
- http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet