Безопасность в Vue приложениях

Вы написали красивое Vue-приложение. Компоненты летают, реактивность работает как часы, а клиент доволен. Но если вы не проверили три конкретных места — всё это может разлететься осколками. Не очередное перечисление очевидных истин, а взгляд на то, о чём молчат в стандартных гайдах.
Вы используете v-html — и знаете, что не стоит. Но что насчёт dangerouslySetInnerHTML-аналогов в сторонних компонентах? Зависимости, которые вы тянете через npm, могут содержать уязвимости, о которых вы даже не догадываетесь. И это не гипотетические риски — реальные кейсы взломов через скомпрометированные пакеты уже стали рутиной. Вам нужно смотреть глубже.
Угроза приходит не только извне. Неправильное управление состоянием, утечка данных через Vuex или Pinia, забытые токены в кеше браузера — это зона вашей ответственности. В этой разборе вы найдёте не просто списки, а конкретные сценарии: как выглядит атака через ваш же код, где на самом деле кроется XSS, и почему стандартный Content Security Policy часто не работает с Vue.
1. XSS в Vue: неочевидные векторы атаки, о которых молчат
Вы уверены, что Vue автоматически экранирует все вставки? Да, {{ }} экранирует HTML-сущности. Но как только вы используете v-html или передаёте строку в директиву href через JavaScript-выражение — ваша защита исчезает. Главная опасность: динамически создаваемые шаблоны через new Function() или строковые шаблоны в вычисляемых свойствах.
Пример: вы делаете :class="userInput" — и злоумышленник передаёт {'xss': 'onerror=alert(1)'}. Vue, конечно, проверяет допустимые значения, но если ваша валидация на входе пропускает объект, атрибуты могут быть подменены. Подобные атаки встречаются реже, но без них на ходу сложно защититься.
- Атака через
v-bindс объектом: если вы привязываете атрибуты через объект{...attrs}, злоумышленник может передатьonloadилиonerrorв атрибуты — браузер выполнит JavaScript. - Уязвимость в
v-model: при использовании с кастомными компонентами, если компонент сам не фильтрует вводимые данные, злонамеренный ввод может привести к инъекции в DOM. - Серверный рендеринг (SSR) с
innerHTML: если вы рендерите строку, полученную от пользователя, черезv-ifилиv-showвнутри SSR-компонента — риск XSS растёт экспоненциально.
Совет: никогда не доверяйте динамическим атрибутам, которые создаются на основе пользовательского ввода. Используйте DOMPurify для санитации любых строк, которые попадают в v-html или динамические v-bind. Но помните: санитация на клиенте — это хорошо, но она не заменяет проверку на сервере.
2. Зависимости npm: невидимая бомба замедленного действия
Стандартный npm audit показывает далеко не всё. Вы можете увидеть сотни предупреждений, но какая уязвимость реально угрожает вашему приложению? Vue-приложения часто используют такие пакеты, как vue-router, vuex, pinia, axios, vue-apollo, element-plus — и у каждого из них есть миллионы загрузок, а значит, каждый является потенциальной мишенью.
Реальный пример: в 2023 году в одном из популярных UI-фреймворков для Vue нашли уязвимость, позволяющую внедрить произвольный атрибут в DOM через компонент кнопки. Пока вы не обновили версию — ваше приложение уязвимо. Более того: если ваша команда использует монорепозиторий с сотнями зависимостей, вы даже не заметите, что какая-то из них тянет устаревшую версию Vue или уязвимую библиотеку.
- Скрытые зависимости: пакет, который вы используете, может тянуть другую библиотеку с уязвимостью.
npm list --depth=10покажет весь граф, но когда вы его последний раз смотрели? - Типичные CVE в Vue-экосистеме: уязвимости в
vue-template-compiler(инъекции через шаблоны),vue-server-renderer(утечка данных через атрибуты),@vue/cli(ведёт к подключению к серверам злоумышленников). - Supply-chain атака: злоумышленник может залить вредоносный пакет с именем, похожим на популярный (тайпсквоттинг). Пример:
ve-routerвместоvue-router.
Что с этим делать? Используйте npm audit fix с умом, но всегда проверяйте changelogs. Добавьте в CI/CD этапы сканирования уязвимостей с помощью Synk или Socket. Заморозьте версии зависимостей в package-lock.json и не обновляйте пакеты автоматически — дайте команде время на тестирование.
3. CSP и Vue: почему настройки часто ломают приложение
Content Security Policy (CSP) — ваш щит от XSS. Но с Vue этот щит может превратиться в подушку безопасности: он даёт ложное спокойствие. Стандартный CSP для Vue-приложений часто требует unsafe-eval для script-src, что убивает весь смысл защиты. Почему? Потому что Vue в режиме разработчика использует new Function() для компиляции шаблонов. Если вы используете сборку Vue, которая включает компилятор в рантайме, CSP-политика с unsafe-eval не заблокирует XSS-инъекцию через v-html — она разрешает выполнение любого кода.
Более тонкий момент: CSP может блокировать инлайн-стили, которые Vue использует для динамического управления классами. Если ваш компонент использует :style с объектом, браузер может не применить стили, если политика запрещает инлайн-атрибуты. Это не уязвимость, но ведёт к поломке интерфейса, что может быть использовано для атаки на логику (например, визуальный DoS).
- Решение для CSP без
unsafe-eval: используйте сборку Vue, которая компилирует шаблоны на этапе сборки (vue-loader), а не в рантайме. Тогда CSP может быть строгим — только'self'и'nonce'. - Nonce для стилей: если используете
:style, генерируйте nonce на сервере и передавайте его через инжекцию. Чтобы Vue не добавлял инлайн-стили без nonce, переопределитеstyleчерез атрибутnonce. - Политика для шрифтов и иконок: многие UI-библиотеки (Element, Ant Design) загружают иконки через
@font-faceс base64-инлайн. CSPfont-srcдолжен разрешать'self'иdata:, иначе шрифты не отобразятся.
Правильная CSP для Vue-приложения: script-src 'self' 'nonce-xyz'; style-src 'self' 'nonce-xyz'; font-src 'self' data:; img-src 'self' data: https:; connect-src 'self' https://api.yoursite.com; object-src 'none'; base-uri 'none';. И никогда не пишите 'unsafe-inline' для скриптов — это прямой путь к XSS.
4. Серверный рендеринг (SSR) и утечка данных: что вы не проверяете
Vue SSR — мощный инструмент, но он открывает дверь для утечек данных, о которых вы можете и не подозревать. Когда вы рендерите страницу на сервере, все данные, которые вы передаёте в шаблон, сериализуются. Если случайно в контекст рендеринга попал объект с чувствительной информацией (токен, пароль, внутренние логи), он будет встроен в HTML в виде window.__INITIAL_STATE__. Клиент заберёт эти данные — и любой может прочитать их через инспектор кода.
Более тонкая угроза: при SSR-рендеринге вы можете вызвать на сервере API-запрос, который возвращает данные для авторизованного пользователя. Если не обработать ошибку, эти данные могут быть вставлены в HTML для всех пользователей, а не только для того, кто имеет к ним доступ. Пример: вы рендерите блок с email-адресом пользователя, но не проверяете, что это именно тот пользователь, который запросил страницу. Бот может собрать все email’ы из HTML.
- Обязательно: весь объект, передаваемый в
renderToString, должен проходить фильтрацию. Удалите все чувствительные поля (токены, ключи, пароли) перед рендерингом. - Не используйте
store.replaceStateнапрямую с данными клиента: если вы передаёте данные от клиента на сервер черезcontext, проверяйте, что вы не раскрываете информацию другому пользователю. - Правильно настраивайте hydrate: если ваше приложение гидрируется (hybrid rendering), убедитесь, что данные для гидрации не содержат приватной информации. Лучше всего — отделите публичные данные от приватных и рендерите приватные через асинхронную загрузку на клиенте.
Совет: всегда используйте helmet (для серверного рендеринга) для заголовков безопасности, но помните, что он не защищает от утечки данных через HTML-контент. Проверяйте, что вы вставляете в window.__INITIAL_STATE__.
5. Vuex/Pinia: опасность реактивного состояния
Вы храните данные в store, и они реактивно доступны каждому компоненту. Это удобно. Но если вы не контролируете, какой компонент может изменить состояние — вы открываете дверь для атаки типа «state poisoning». Злоумышленник может внедрить вредоносное значение в атрибут компонента, и через реактивность оно распространится по всему приложению.
Пример: у вас есть форма для редактирования профиля. Пользователь отправляет данные, которые попадают в Pinia. Если вы не валидируете входные данные на стороне клиента (хотя это должно быть на сервере), то злоумышленник может сохранить JavaScript-строку в поле name. Когда другой пользователь просматривает профиль, v-html на старнице может вывести эту строку — и сработает XSS. Ваш store стал вектором атаки.
Ещё одна ловушка: мутации из плагинов. Вы используете плагин для авторизации (например, vue-router с guard). Если плагин изменяет состояние store (например, сохраняет роль пользователя), и вы не проверяете, что роль получена от доверенного источника, злоумышленник может подменить запрос и установить себе роль администратора. Это не гипотетика — такая уязвимость найдена в реальных проектах.
- Никогда не передавайте в store данные напрямую от пользователя без санитации: все строки должны проходить через escape-функции (как минимум
sanitize-html). - Контролируйте права на запись: используйте отдельные действия (actions) для записи в state, которые проверяют, имеет ли текущий пользователь право на изменение. Не разрешайте компонентам напрямую вызывать мутации.
- Используйте TypeScript для строгой типизации state: это снижает риск инъекции некорректного типа (например, вместо числа — строка с HTML-кодом).
Хорошая практика: в Pinia настройте строгий режим strict: true в разработке — это выявит нежелательные изменения через devtools. Но не полагайтесь на это в продакшне: используйте deepFreeze для глубокой заморозки state после мутации.
В конечном счёте, безопасность вашего Vue-приложения стоит на трёх китах: контроль входных данных и атрибутов, управление зависимостями и грамотная CSP. Каждый из этих аспектов требует внимания не разово, а на протяжении всего цикла разработки. Не думайте, что если ваш код работает без ошибок — он защищён. Проверьте каждую точку, где пользовательский ввод касается реактивной системы. Только тогда вы сможете спать спокойно.
Добавлено: 23.04.2026
