Защита от XSS в PHP

Александр Кичатов

Что такое 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 определённую последовательность символов, например &copy;, а браузер отображает соответствующий этой мнемонике символ, в данном случае значок копирайта ©.

Попробуй сам:

<div>
Абзац &para; <br>
Перевёрнутый знак вопроса &iquest; <br>
Знак умножения (крестик) &times; <br>
Стрелка влево &larr; <br>
Типографский крестик &dagger;
</div>

Так вот. Когда мы запускаем функцию htmlspecialchars(), она берёт нашу строку и заменяет некоторые символы в ней (кавычки, угловые скобки и т.д.) на мнемоники, чтобы браузер гарантированно вывел нашу строку на экран как строку, не пытаясь выполнять её как код.

Т.е. когда мы введём в нашу форму текст <script>alert('hello')</script>, функция htmlspecialchars() превратит его в &lt;script&gt;alert('hello')&lt;/script&gt;. Разумеется, браузер уже не воспримет такой код как 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 символов, но ООО &quot;Три кота&quot; - уже 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> <!-- Без обработки -->

Комментарии