Проблемы с безопасностью могут привести к утечке данных пользователей или даже к полному уничтожению сайта. Исследования показывают, что большинство сайтов имеют проблемы с безопасностью и уязвимостью к атакам. Время от времени случаются громкие взломы и утечки данных сотен тысяч и миллионов пользователей.
Безопасность веб-приложений — важная тема, которой мы уделим весь этот урок.
Главное правило безопасности
Оно звучит так:
Никогда не доверяйте пользователям
В первую очередь, это правило касается данных, которые пользователи вводят на сайте. Предположим, что у нас есть страница профиля пользователя, где выводится его идентификатор, взятый из адресной строки:
import fastify from 'fastify'
const app = fastify()
const port = 3000
app.get('/users/:id', (req, res) => {
res.type('html')
res.send(`<h1>${req.params.id}</h1>`)
})
app.listen({ port }, () => {
console.log(`Example app listening on port ${port}`)
})
Когда код реализует эту функциональность, он рассчитывает, что в адресе используются только допустимые имена. А теперь попробуем открыть такой адрес:
http://localhost:3000/users/%3Cscript%3Ealert('attack!')%3B%3C%2Fscript%3E
Если мы доверимся данным от пользователей и откроем такой адрес, случится вот это:
В этом адресе закодирован JavaScript-код, который в оригинале выглядит так:
<script>
alert('attack!');
</script>
Проблема в том, что код не отобразился, но попал в HTML страницы и выполнился. Так произошло, потому что для браузера такой JavaScript-код выглядит как часть страницы.
Если открыть получившийся HTML, то он будет выглядеть так:
<h1>
<script>alert('attack!');</script>
</h1>
Именно так происходит XSS-атака (межсайтовый скриптинг). На страницу внедряется вредоносный код, который выполняется в браузере пользователя и отправляет информацию о пользователе на сервер злоумышленника. XSS — это одна из самых распространенных атак. Очень много подобных уязвимостей есть даже на сайтах больших компаний.
Специфика подобных атак в том, что вредоносный код может использовать авторизацию пользователя в веб-системе. Так злоумышленник может получить расширенный доступ к системе или логины и пароли пользователей. Если в исходном коде встречается конструкция <текст>
, то браузер автоматически считает ее тегом.
Вернемся к коду из начала урока:
import fastify from 'fastify'
const app = fastify()
const port = 3000
app.get('/users/:id', (req, res) => {
res.type('html')
res.send(`<h1>${req.params.id}</h1>`)
})
app.listen({ port }, () => {
console.log(`Example app listening on port ${port}`)
})
Здесь мы выводим данные без какой-либо предварительной обработки. В таком случае браузер пытается интерпретировать как HTML все, что похоже на HTML. Любой пользователь может внедрить на сайт исполняемый JavaScript-код без нашего ведома. Другими словами, мы проявили доверие к пользовательским данным и создали уязвимость.
Чтобы закрыть ее, нужно использовать не сами теги, а HTML-эквиваленты символов. Тогда код выше начнет выглядеть так:
<h1>
<script>alert('attack!');</script>
</h1>
Здесь мы заменили:
<
на<
>
на>
Это не экранирование, а именно замена спецсимволов на их HTML-эквиваленты. Если открыть браузер, то там мы увидим правильное отображение:
Замена символов на спецсимволы выполняется с помощью специальных библиотек, например, sanitize-html. Один из вариантов выглядит так:
# Установка пакета
npm i sanitize-html
import fastify from 'fastify'
import sanitize from 'sanitize-html'
const app = fastify()
const port = 3000
app.get('/users/:id', (req, res) => {
const escapedId = sanitize(req.params.id)
res.type('html')
res.send(`<h1>${escapedId}</h1>`)
})
app.listen({ port }, () => {
console.log(`Example app listening on port ${port}`)
})
Функция sanitize()
принимает на вход HTML и заменяет в нем все спецсимволы на их HTML-эквиваленты. Остальные символы остаются без изменения.
Через такую обработку нужно пропускать любые данные, которые мы выводим. Исключение составляют лишь ситуации, в которых мы точно знаем, что в данных есть HTML и мы его хотим отобразить. К таким данным могут относиться статьи в блоге, потому что они содержат HTML-часть. Например, так работает блог Хекслета.
Самостоятельная работа
- Добавьте в приложение маршрут, который выводит идентификатор пользователя, основываясь на данных из строки запроса. Повторите шаги из урока. Экранируйте вывод идентификатора пользователя на странице пользователя по аналогии, как сделано в теории урока
- Убедитесь на собственном опыте, что шаблонизатор защищает от XSS атак. Измените обработчик таким образом, чтобы не экранированные данные пользователя отправлялись в шаблон и происходил его рендеринг. Запустите приложение и попробуйте передать в строке запроса какой-нибудь HTML. Проверьте, что данные в шаблоне проходят предварительную обработку автоматически
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.