Что такое XSS
XSS - это когда злоумышленник пытается через формы на сайте (обратная связь, оформление заказа и т.п.) добавить свой javascript-код, который затем выполнится в браузере админа/менеджера сайта или других пользователей и натворит дел.
Как это работает
Возьмём простую форму обратной связи. 2 поля для заполнения и кнопка отправки.
<form method="POST">
<p>Заголовок: <input name="title"></p>
<p>Текст: <textarea name="content"></textarea></p>
<p><button name="form">Отправить!</button></p>
</form>
Добавим к этой форме PHP обработчик, который просто выводит заголовок и текст на экран:
<?php
if(isset($_POST['form'])) {
echo 'Заголовок: ', $_POST['title'], '<br>';
echo 'Текст: ', $_POST['content'];
}
?>
<form method="POST">
<p>Заголовок: <input name="title"></p>
<p>Текст: <textarea name="content"></textarea></p>
<p><button name="form">Отправить!</button></p>
</form>
Прекрасно. Код работает, введённый текст выводится над формой.
А теперь вместо текста введём какой-нибудь javascript код, например <script>alert('hello')</script>.
При отправке формы этот код выполнится браузером. В этом и заключается дыра в безопасности. Только обычно мы выводим текст не сразу после отправки формы, а сначала сохраняем его в базу, затем выводим на разных страницах сайта.
Чем опасна XSS
Внедрив свой скрипт, злоумышленник получает доступ ко всей html-странице, может её читать и менять как угодно.
Кроме этого, злоумышленник получает доступ к браузерным кукам пользователя. Разумеется, только тем, которые относятся к текущему сайту. Он может украсть куки, отвечающие за авторизацию пользователя, и подставить их в свой браузер.
Таким образом, он может войти на сайт под чужой учётной записью без логина и пароля. Разумеется, только если на сайте нет других проверок: на соответствие браузера, IP-адреса и т.д., хотя при желании их можно подделать.
Как защититься от XSS
К нашему огромному счастью, есть простой универсальный инструмент - функция htmlspecialchars() или её иногда применяемый аналог htmlentities().
Как это работает. В HTML есть такая штука как сущности или мнемоники. Это когда я пишу прямо в HTML определённую последовательность символов, например ©, а браузер отображает соответствующий этой мнемонике символ, в данном случае значок копирайта ©.
Попробуй сам:
<div>
Абзац ¶ <br>
Перевёрнутый знак вопроса ¿ <br>
Знак умножения (крестик) × <br>
Стрелка влево ← <br>
Типографский крестик †
</div>
Так вот. Когда мы запускаем функцию htmlspecialchars(), она берёт нашу строку и заменяет некоторые символы в ней (кавычки, угловые скобки и т.д.) на мнемоники, чтобы браузер гарантированно вывел нашу строку на экран как строку, не пытаясь выполнять её как код.
Т.е. когда мы введём в нашу форму текст <script>alert('hello')</script>, функция htmlspecialchars() превратит его в <script>alert('hello')</script>. Разумеется, браузер уже не воспримет такой код как javascript и просто выведет на экран как есть.
Проверим:
<?php
if(isset($_POST['form'])) {
echo 'Заголовок: ', htmlspecialchars($_POST['title'], ENT_QUOTES, 'UTF-8'), '<br>';
echo 'Текст: ', htmlspecialchars($_POST['content'], ENT_QUOTES, 'UTF-8');
}
?>
<form method="POST">
<p>Заголовок: <input name="title"></p>
<p>Текст: <textarea name="content"></textarea></p>
<p><button name="form">Отправить!</button></p>
</form>
Теперь какой бы javascript код мы не пытались подставить, он будет просто выводиться в браузер как строка.
Когда лучше обрабатывать строку
В интернете часто спорят о том, когда лучше обрабатывать текст, до записи в базу данных или при выводе на экран.
У обработки до записи в базу есть несколько недостатков:
- Мы не можем узнать реальную длину строки, поскольку ООО "Три кота" - это 14 символов, но ООО "Три кота" - уже 24 символа.
- Помимо HTML данные из базы иногда нужно подставлять куда-то ещё, например в word/excel/pdf файлы, где может не быть никаких мнемоник. Придётся декодировать все данные в исходный вид.
В общем, неудобно это. Рекомендую всегда сохранять в базу исходный текст, который ввёл пользователь, а обрабатывать уже при выводе на экран.
Как упростить обработку строк
И что, каждый раз теперь писать эту длиннющую функцию?
<div>
<?= htmlspecialchars($_POST['title'], ENT_QUOTES, 'UTF-8') ?>
</div>
Ну нет, спасибо. Лучше напишем отдельную функцию под это дело:
function e($string)
{
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
Можно заменить return сразу на echo, не принципиально. Теперь обрабатывать строки гораздо проще:
if(isset($_POST['form'])) {
echo 'Заголовок: ', e($_POST['title']), '<br>';
echo 'Текст: ', e($_POST['content']);
}
Если ты слышал про шаблонизаторы вроде Twig или Blade, там специально используется свой синтаксис вывода переменных, чтобы они по-умолчанию всегда обрабатывались:
<div>{{ title }}</div> <!-- С обработкой -->
<div>{!! content !!}</div> <!-- Без обработки -->