Join our Newsletter — 33% off our NHI Course
Home› Guides› Vue Security Guide: Template Safety, XSS and Build…
Guide Application Security

Vue Security Guide: Template Safety, XSS and Build Hygiene

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 26 October 2026 13 min read
On this page

Vue escapes everything you interpolate, so a Vue application becomes exploitable mainly when untrusted content is treated as a template, rendered with v-html, bound into a URL, style or event attribute, or leaked through the build and dev tooling around it. The Vue team's own position is blunt: XSS through template expressions is not a framework vulnerability, because it only happens when a developer compiles untrusted content as a template. That puts the responsibility on application teams. This guide covers what Vue does for you, the specific patterns that defeat it, server-side rendering risks such as cross-request state pollution, and the Vite and npm hygiene that keeps secrets and malicious code out of the bundle. It is for Vue and Nuxt developers, application security engineers and code reviewers; for XSS theory in general, see the Cross-Site Scripting (XSS) Prevention Guide.

Key takeaways

  • Templates are code. Vue compiles templates to JavaScript, so concatenating user input into a template string, or mounting Vue over server-rendered HTML that contains user content, hands the attacker code execution, on the server too when SSR is used.
  • v-html is the main sink. Along with innerHTML in render functions and JSX, it is the one place Vue deliberately skips escaping; anything bound there must be sanitised or fully trusted.
  • URLs, styles and event attributes need your own controls. Vue escapes attribute values but does not stop a javascript: URL, hostile CSS or a bound onclick string.
  • SSR adds server-side risks. Shared module state leaks one user's data to another, and serialised initial state must be escaped before it is embedded in HTML.
  • The build is part of the attack surface. Vite only exposes VITE_ prefixed variables to client code, but everything that is exposed is public, a dev server opened to the network has had a KEV-listed file disclosure flaw, and Vue 2 has had no fixes since 31 December 2023.

What Vue protects and where it stops

Vue's protection comes from using browser APIs that cannot create markup. Text interpolation is written with textContent and dynamic attributes with setAttribute, so a value containing <script> or a closing quote is rendered as text. The table shows where that guarantee holds and where it does not.

BindingVue behaviourRemaining riskControl
{{ value }}Escaped (textContent)None from the frameworkDefault; prefer it
:title="value" and other plain attributesEscaped (setAttribute)None for inert attributesDefault
v-html, h('div', { innerHTML }), JSX innerHTMLRendered as raw HTMLStored and reflected XSSSanitise with a maintained library, or render structured data instead
:href, :src, :action, :formactionEscaped but not scheme-checkedjavascript: and data: URLs, open redirectsValidate scheme and host on the backend before storing; allowlist on output
:styleApplied as givenOverlay and clickjacking style attacksObject syntax with only specific properties user-controllable
:onclick, :onfocus and other event attributes bound to stringsSet as attribute valuesScript executionNever bind user data to event attributes; use @click handlers
template: option or in-DOM template built from user inputCompiled and executedArbitrary JavaScript in the browser, and on the server under SSRTemplates only from source control

Vue also refuses to render <style> tags inside templates, which removes one style injection route, and discourages rendering <script> elements entirely because templates should have no side effects.

Template injection: Vue's rule number one

Vue's security documentation calls "never use non-trusted content as your component template" the most fundamental rule. Expressions in a template are evaluated during rendering, and the framework cannot sandbox them without unacceptable cost. Two shapes cause real incidents:

  • String-built templates: code such as createApp({ template: '<div>' + userInput + '</div>' }), or a CMS feature that lets editors write "Vue snippets". The fix is architectural: user content is data passed as props, never template source.
  • Mounting over server output: a server-rendered page (PHP, Django, Rails, a CMS) that is then mounted as a Vue app. HTML that the server safely escaped can still contain {{ }} expressions or directives that Vue will evaluate. Vue's guidance is to never mount on nodes that may contain server-rendered, user-provided content; mount on a dedicated empty element instead. If a page must mix the two, wrap server-rendered user content in an element marked with v-pre so Vue skips compilation for it, and treat that as a reviewed exception.
  • Remove the compiler where you can: the default Vite and Vue CLI setups use the runtime-only build (vue.runtime.*) because single-file component templates are precompiled. Aliasing vue to vue/dist/vue.esm-bundler.js ships the template compiler to the browser and reopens runtime compilation; treat that alias as a security review trigger. The full build compiles templates with new Function, so it also needs 'unsafe-eval' in a Content Security Policy, which weakens the policy for the whole page.

Rendering rich content safely

Features such as comments, product descriptions and help articles often need formatted text. The choice of approach decides how much residual risk remains.

ApproachWhen to choose itTrade-off
Structured data rendered by components (for example a JSON document tree or Markdown parsed to an AST and rendered with components)New features; you control the formatNo HTML sink at all; more build effort
Markdown rendered to HTML, then sanitised, then v-htmlUsers write MarkdownConfigure the Markdown library to disable raw HTML; sanitise the output anyway
User HTML sanitised server side on write, sanitised again client side on renderWYSIWYG editors, imported contentTwo sanitiser passes; keep the libraries updated
Untrusted HTML in a sandboxed <iframe sandbox> on a separate originFull HTML, CSS or script from users is a product requirementThe only model Vue's documentation treats as safe for fully untrusted content
  • Wrap the sink: create one SafeHtml component or v-safe-html directive that calls DOMPurify.sanitize() with a restrictive profile (for example USE_PROFILES: { html: true }), and ban raw v-html elsewhere with the vue/no-v-html rule from eslint-plugin-vue.
  • Sanitise for the final context: sanitising and then modifying the string (string replacement, highlighting search terms) can reintroduce markup. Sanitise last.
  • URLs on the backend: Vue's documentation says user-provided URLs should be sanitised by the backend before they are saved, so every client, including mobile apps, is protected; front-end URL sanitising alone means the problem already exists. See the Input Validation and Output Encoding Guide.
  • Styles: if users choose colours or backgrounds, bind them through object syntax such as :style="{ color: userColour }" rather than a free-form style string. See the Clickjacking Guide.

Server-side rendering and Nuxt

With SSR, the same components run on a Node.js server for every request. Two Vue-specific risks appear that do not exist in a purely client-side app.

  • Cross-request state pollution: module-level reactive state is a singleton on the server, initialised once at boot and reused across requests. If one user's data is written there, the next request can render it. Vue's guidance is to create a new app instance, router and store per request and provide state through app-level provide; Pinia is designed for this model. Review any export const state = reactive(...) in code that runs under SSR.
  • Serialised initial state: the server embeds store state in the HTML so the client can hydrate. Pinia's SSR documentation stresses that this state must be escaped, recommending devalue (used by Nuxt). A naive JSON.stringify placed inside a <script> tag lets a value containing </script> break out. Also review what is serialised: tokens, internal IDs and other users' data should never be in it.
  • Hydration mismatch is not a security control: Vue 3.5 adds data-allow-mismatch to suppress expected mismatches. Do not use it to hide a mismatch caused by content you have not traced.
  • Server secrets: SSR code can read server environment variables. Keep that code in server-only modules so it cannot be imported into the client bundle.
  • Route guards: Vue Router's beforeEach and beforeEnter guards control navigation in the browser. They are a user experience feature; every API they appear to protect must enforce authorisation itself.

Vite build and dev server hygiene

Most Vue 3 projects build with Vite, and its configuration decides what reaches the browser and who can reach the developer's machine.

SettingDefaultRisk if changedRecommendation
server.host'localhost'--host or 0.0.0.0 exposes the dev server to the networkKeep the default; use a tunnel with authentication if remote access is needed
server.allowedHostslocalhost, .localhost subdomains and IP addressestrue allows DNS rebinding, letting any website read your sourceExplicit list only, never true
server.fs.stricttrueFiles outside the workspace become readable via /@fs/Leave enabled
server.fs.deny.env, .env.*, certificate and key files, .npmrc, .yarnrc.yml, **/.git/**Overriding the list can expose secretsExtend it, do not replace it
server.corsOrigins on localhost, 127.0.0.1 and ::1Broad CORS lets other sites read dev server responsesKeep the default
VITE_ prefixed environment variablesOnly these reach import.meta.env in client codeA secret given the prefix ships in the bundleNever put secrets in VITE_ variables

The dev server is not a theoretical risk: CISA added Vite CVE-2025-31125 to the Known Exploited Vulnerabilities catalogue in January 2026. It exposed the content of non-allowed files, and only applications that explicitly exposed the dev server to the network with --host or server.host were affected. Keep Vite patched and the dev server on localhost. In production, serve only the built dist output, decide deliberately whether source maps are published, and make sure .git and environment files are not deployed (ASVS V13.4.1). Configure a Content Security Policy with nonces or hashes and frame-ancestors at the server or CDN; see the Content Security Policy Guide and the Security Headers Guide.

Dependencies, supply chain and versions

  1. Lock installs: commit the lockfile and install with npm ci (or the pnpm and Yarn equivalents with frozen lockfiles) in CI.
  2. Slow down and review updates: configure a cooldown on Dependabot or Renovate and require a human to approve new packages and install scripts. In the axios npm compromise, a hijacked maintainer account published versions whose dependency ran a backdoor at install time, and dependency bots opened upgrade pull requests within minutes, some merged with no human involved. Front-end projects install axios very widely, so this is a realistic Vue scenario.
  3. Audit plugins and component libraries: Vue's documentation recommends reviewing dependencies that render to the DOM for dangerous patterns such as internal v-html or innerHTML use.
  4. Pin CDN scripts with SRI: no-build Vue pages that load Vue or plugins from a CDN must pin exact versions and add integrity attributes.
  5. Retire Vue 2: Vue 2 reached end of life on 31 December 2023 and receives no security or browser compatibility fixes from the Vue team. Plan migration to Vue 3, or treat any commercial extended support as a time-limited bridge.
  6. Stay current on Vue 3: Vue has no fixed release cycle and ships patches as needed; the Vue team recommends always using the latest versions of Vue and its official companion libraries. Do not run alpha, beta or RC builds in production. See the Malicious Package and Dependency Confusion Guide.

Implementation roadmap

  1. Inventory: list Vue applications with version (2 or 3), rendering mode (SPA, SSR, Nuxt, no-build), Vite version and hosting.
  2. Find the sinks: grep for v-html, innerHTML, template:, vue.esm-bundler, :href bound to user data, :style strings and module-level reactive( in SSR code.
  3. Fix in order of exposure: template injection and mounting over server content first, then unsanitised v-html on user data, then URL and style bindings.
  4. Enable lint gates: turn on vue/no-v-html as an error with a documented exception for the central safe component.
  5. Deploy CSP in report-only: measure for two to four weeks, then enforce in rings (internal, a share of traffic, everyone). Roll back by reverting to report-only at the CDN, without a code release.
  6. Harden SSR: per-request app and store instances, escaped state serialisation, server-only modules for secrets.
  7. Harden the toolchain: Vite patched and bound to localhost, dependency cooldowns, SRI on CDN assets, and a dated Vue 2 exit plan.

Detection and measurement

  • CSP violation reports: new inline script or unknown script origins after a release usually mean a new sink or a compromised dependency.
  • CI signals: lint failures for vue/no-v-html, new install scripts in the lockfile diff, and advisories from npm audit or your composition analysis tool.
  • Server logs: for SSR, watch for responses containing another session's identifiers in tests, and alert on unexpected hosts reaching dev or preview servers.
MetricTarget
Raw v-html uses outside the reviewed safe componentZero
Applications shipping the runtime template compiler in productionZero, or each with a documented reason
Production Vue apps with an enforced nonce or hash based CSP100%
Vue 2 applications still in productionFalling to zero against a dated plan
Dependency updates merged without human reviewZero for production branches

Common failure patterns

  • "It's only admin content": v-html renders CMS fields on the assumption that only staff edit them, until a lower-privileged role or an imported feed can write to the same field.
  • Vue sprinkled over a legacy page: mounting on #app wrapped around a whole server-rendered page turns any user comment containing {{ }} into template code.
  • Secrets with a VITE_ prefix: a payment or AI API key is given the prefix "to make it work" and ships to every browser. See the Secrets Scanning Buyer's Guide for catching these in repositories.
  • Global store under SSR: a module-level store survives between requests and renders the previous user's profile.
  • Dev server on the network: vite --host left running on a laptop on shared Wi-Fi, or server.allowedHosts: true added to fix a tunnel.
  • Unreviewed bot merges: auto-merged dependency updates install a malicious package on developer machines and CI runners, where npm and cloud tokens live.

Framework mapping

Control areaOWASP ASVS 5.0NIST SP 800-53 Rev 5NIST CSF 2.0 / ATT&CK
Safe rendering, sanitisation of rich textV1.2.1, V1.3.1, V3.2.2SI-10, SI-15PR.PS-06; mitigates T1059.007 JavaScript
Content Security Policy and framingV3.4.3, V3.4.6SI-10T1189 Drive-by Compromise
CDN assets with SRI, dependency controlV3.6.1, V15.2.1SR-3, RA-5T1195.002 Compromise Software Supply Chain; ID.RA-01
No source control metadata or secrets deployedV13.4.1SA-15PR.PS-06
Developer testing of templates and SSRV1.3.3SA-11PR.PS-06

Practitioner checklist

  • Confirm no template is built from user input and no Vue app mounts over server-rendered user content.
  • Route every v-html through one sanitising component and enforce vue/no-v-html in CI.
  • Validate URL schemes and hosts on the backend before storing user-supplied links.
  • Bind user-chosen styles only through object syntax with fixed properties; never bind event attributes to user strings.
  • Use the runtime-only build in production and avoid 'unsafe-eval' in CSP.
  • Under SSR, create the app, router and Pinia store per request and serialise state with an escaping serialiser.
  • Keep the Vite dev server on localhost, never set server.allowedHosts: true, and keep Vite patched.
  • Audit VITE_ variables and the built bundle for secrets before each release.
  • Lock dependencies, add an update cooldown and human review, and use SRI for CDN scripts.
  • Track every Vue 2 application to a dated migration plan.

Standards and references

Related NHI Mgmt Group resources: Cross-Site Scripting (XSS) Prevention Guide · Angular Security Guide · React and Next.js Security Guide · Client-Side Security Guide · npm Tokens Security Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 26 October 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.