Без чего не может обойтись не одно фронтенд-приложение? Верно, без форм ввода данных. При всем этом, наборы инпутов часто становятся самым тонким местом в приложении, через которое злоумышленники получают доступ к данным другим пользователей, а затем делают с ними все что им заблагорассудится. Самый популярный метод атаки — XSS (Cross-Site Scripting) или «межсайтовый скриптинг»
Звучит страшно, но не стоит паниковать. И браузеры постоянно работают над безопасностью, и мы не лыком шиты. Давайте разбираться.
- Самый простой пример XSS
- Мелкое хулиганство или серьезная проблема?
- Три типа XSS. Знать врага в лицо
- 1. Отраженный (Reflected XSS)
- 2. Хранимый (Stored XSS)
- 3. DOM-based XSS
- Как правильно?
- 1. Всегда используй textContent вместо innerHTML
- 2. Экранируем пользовательский ввод
- 3. Используем Content Security Policy (CSP)
- Итого
Самый простой пример XSS
Допустим, у нас есть поле ввода комментария. Это какой-то базовый <div>, в который скриптом подставляется текст из инпута.
Код может выглядеть как-то так
const userInput = dosument.getElementById('input').value;
document.getElementById('comments').innerHTML = userInput;
И все даже более-менее работает, но ровно до тех пор пока не приходит хитрый хакер и не вводит в инпут что-то вроде
'<img src="x" onerror="alert(\'ВЗЛОМАНО\')">'
Естественно, мы захотим чтобы комментарии не пропадали после обновления страницы, и мы запишем этот код где-то в БД. Затем, когда какой-либо пользователь загрузит наш пост, загрузятся и комментарии и этот алерт снова отработает.
Мелкое хулиганство или серьезная проблема?
Алерт — это немного унизительно для создателя приложения, но ничуть не критично для его пользователей. И если бы дело ограничивалось только всплывающими сообщениями, злоумышленники не тратили бы на это свое время.
Но вот что нужно понимать. Таким способом потенциально вредоносный JS загружается в браузер пользователя и исполняется в нем же, то есть код злоумышленника может незримо «наблюдать» за всем, что пользователь делает в вашем приложении. Пользуясь этим незаконным преимуществом, он может
- Украсть куки. Если у кук нет флага
HttpOnly, хацкер может переслать себе данные сессионной куки и войти в аккаунт пользователя, даже не зная пароля - Угнать данные форм. Он может подменить поле ввода логина/пароля и отправить данные себе
- Подделывать действия. «Заставить» пользователя лайкнуть пост, подписаться на какой-нибуд канал и т.д.
- Следить за нажатиями клавиш. Грубо говоря, «логировать себе» все, что пользователь печатает на клавиатуре.
И вот где проблема становится не просто серьезной, а критической. Так как вредоносный скрипт выполняется от имени текущего пользователя, под угрозой в том числе и админские учетки.
Три типа XSS. Знать врага в лицо
1. Отраженный (Reflected XSS)
Хацкер отправляет условному пользователю проекта, где есть строка поиска, которая берет данные из урла, ссылку вида:https://somesite.com/search?q=<script>alert(1)</script>
Пользователь переходит по ссылке. Сервер собирает страницу с учетом значения параметра q, возвращает (отражает) ее пользователю уже со встроенными вредоносным кодом.
Как защититься: Никогда не выводите данные из URL напрямую в HTML.
2. Хранимый (Stored XSS)
Пожалуй, самый опасная разновидность. Хацкер оставляет комментарий в блоге, а внутри комментария — скрипт. Он сохраняется в базе данных.
Когда любой другой пользователь заходит на страницу с этим комментарием — скрипт выполняется у него автоматически.
Как защититься: Валидировать и фильтровать всё, что приходит от пользователя, прежде чем сохранить в базу и прежде чем показать на экране.
3. DOM-based XSS
В отличие от отраженного XSS, проблема возникает уже не на сервере, а прямо в браузере из-за JavaScript.
Например, мы берем значение из window.location.hash или window.location.search и вставляем его через .innerHTML.
const hash = location.hash.slice(1);
document.getElementById('welcome').innerHTML = `Привет, ${hash}`;
Если пользователь зашел по ссылке mysite.com/#<img src=x onerror=alert(1)>, то атака свершилась
Как правильно?
Хорошая новость: защититься несложно. Нужно запомнить несколько важнейших правил.
1. Всегда используй textContent вместо innerHTML
// ПЛОХО (опасно)
element.innerHTML = userInput;
// ХОРОШО (безопасно)
element.textContent = userInput;
textContent вставит текст как есть. Даже если там <script>, он превратится в обычную строку и не выполнится.
2. Экранируем пользовательский ввод
Если без HTML никак (нужно выделить жирным или вставить ссылку) — не стоит изобретать велосипед. В современных библиотеках это работает под капотом.
- React: JSX сам экранирует всё, что в
{}. Но при использованииdangerouslySetInnerHTML, карета превращается в тыкву. - Vue: Аналогично. Если
{{ variable }}экранирует, тоv-html— потенциально опасен.
Без библиотек и фреймворков имеет смысл использовать библиотеку наподобие he или своими руками написать экранирование символов: заменяем < на <, > на > и т.д.
3. Используем Content Security Policy (CSP)
Это как табличка на двери: «Своим можно, чужим — нет». Чтобы магия заработала, на сервере нужно настроить заголовок Content-Security-Policy: default-src 'self'
Так мы запретим выполнение любых скриптов, которые загружены не с текущего домена.
Итого
- Никогда не доверяй пользователю. Любой ввод данных может быть источником опасности.
- Не используй
innerHTMLдля отображения данных от пользователя. Вместо этого используйtextContent. - Не используй
eval(). Интересно, кто-то кроме меня еще помнит про это зло? - Минимирузуй dangerouslySetInnerHTML . Злоупотребление этим и другим подобным в библиотеках и фреймворках — ред флаг. Использовать конечно можно, но только после санитайзинга ввода.
- Установи CSP. Это важнейших рубеж обороны от XSS-атак.
Желаю успехов!







