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
templatestring, 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-htmlis the main sink. Along withinnerHTMLin 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 boundonclickstring. - 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.
| Binding | Vue behaviour | Remaining risk | Control |
|---|---|---|---|
{{ value }} | Escaped (textContent) | None from the framework | Default; prefer it |
:title="value" and other plain attributes | Escaped (setAttribute) | None for inert attributes | Default |
v-html, h('div', { innerHTML }), JSX innerHTML | Rendered as raw HTML | Stored and reflected XSS | Sanitise with a maintained library, or render structured data instead |
:href, :src, :action, :formaction | Escaped but not scheme-checked | javascript: and data: URLs, open redirects | Validate scheme and host on the backend before storing; allowlist on output |
:style | Applied as given | Overlay and clickjacking style attacks | Object syntax with only specific properties user-controllable |
:onclick, :onfocus and other event attributes bound to strings | Set as attribute values | Script execution | Never bind user data to event attributes; use @click handlers |
template: option or in-DOM template built from user input | Compiled and executed | Arbitrary JavaScript in the browser, and on the server under SSR | Templates 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 withv-preso 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. Aliasingvuetovue/dist/vue.esm-bundler.jsships the template compiler to the browser and reopens runtime compilation; treat that alias as a security review trigger. The full build compiles templates withnew 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.
| Approach | When to choose it | Trade-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 format | No HTML sink at all; more build effort |
Markdown rendered to HTML, then sanitised, then v-html | Users write Markdown | Configure the Markdown library to disable raw HTML; sanitise the output anyway |
| User HTML sanitised server side on write, sanitised again client side on render | WYSIWYG editors, imported content | Two sanitiser passes; keep the libraries updated |
Untrusted HTML in a sandboxed <iframe sandbox> on a separate origin | Full HTML, CSS or script from users is a product requirement | The only model Vue's documentation treats as safe for fully untrusted content |
- Wrap the sink: create one
SafeHtmlcomponent orv-safe-htmldirective that callsDOMPurify.sanitize()with a restrictive profile (for exampleUSE_PROFILES: { html: true }), and ban rawv-htmlelsewhere with thevue/no-v-htmlrule fromeslint-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 anyexport 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 naiveJSON.stringifyplaced 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-mismatchto 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
beforeEachandbeforeEnterguards 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.
| Setting | Default | Risk if changed | Recommendation |
|---|---|---|---|
server.host | 'localhost' | --host or 0.0.0.0 exposes the dev server to the network | Keep the default; use a tunnel with authentication if remote access is needed |
server.allowedHosts | localhost, .localhost subdomains and IP addresses | true allows DNS rebinding, letting any website read your source | Explicit list only, never true |
server.fs.strict | true | Files 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 secrets | Extend it, do not replace it |
server.cors | Origins on localhost, 127.0.0.1 and ::1 | Broad CORS lets other sites read dev server responses | Keep the default |
VITE_ prefixed environment variables | Only these reach import.meta.env in client code | A secret given the prefix ships in the bundle | Never 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
- Lock installs: commit the lockfile and install with
npm ci(or the pnpm and Yarn equivalents with frozen lockfiles) in CI. - 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.
- Audit plugins and component libraries: Vue's documentation recommends reviewing dependencies that render to the DOM for dangerous patterns such as internal
v-htmlorinnerHTMLuse. - Pin CDN scripts with SRI: no-build Vue pages that load Vue or plugins from a CDN must pin exact versions and add
integrityattributes. - 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.
- 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
- Inventory: list Vue applications with version (2 or 3), rendering mode (SPA, SSR, Nuxt, no-build), Vite version and hosting.
- Find the sinks: grep for
v-html,innerHTML,template:,vue.esm-bundler,:hrefbound to user data,:stylestrings and module-levelreactive(in SSR code. - Fix in order of exposure: template injection and mounting over server content first, then unsanitised
v-htmlon user data, then URL and style bindings. - Enable lint gates: turn on
vue/no-v-htmlas an error with a documented exception for the central safe component. - 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.
- Harden SSR: per-request app and store instances, escaped state serialisation, server-only modules for secrets.
- 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 fromnpm auditor 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.
| Metric | Target |
|---|---|
Raw v-html uses outside the reviewed safe component | Zero |
| Applications shipping the runtime template compiler in production | Zero, or each with a documented reason |
| Production Vue apps with an enforced nonce or hash based CSP | 100% |
| Vue 2 applications still in production | Falling to zero against a dated plan |
| Dependency updates merged without human review | Zero for production branches |
Common failure patterns
- "It's only admin content":
v-htmlrenders 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
#appwrapped 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 --hostleft running on a laptop on shared Wi-Fi, orserver.allowedHosts: trueadded 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 area | OWASP ASVS 5.0 | NIST SP 800-53 Rev 5 | NIST CSF 2.0 / ATT&CK |
|---|---|---|---|
| Safe rendering, sanitisation of rich text | V1.2.1, V1.3.1, V3.2.2 | SI-10, SI-15 | PR.PS-06; mitigates T1059.007 JavaScript |
| Content Security Policy and framing | V3.4.3, V3.4.6 | SI-10 | T1189 Drive-by Compromise |
| CDN assets with SRI, dependency control | V3.6.1, V15.2.1 | SR-3, RA-5 | T1195.002 Compromise Software Supply Chain; ID.RA-01 |
| No source control metadata or secrets deployed | V13.4.1 | SA-15 | PR.PS-06 |
| Developer testing of templates and SSR | V1.3.3 | SA-11 | PR.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-htmlthrough one sanitising component and enforcevue/no-v-htmlin 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
- Vue.js: Security
- Vue.js: Server-Side Rendering
- Vue.js: Vue 2 End of Life
- Vite: Server Options
- Pinia: Server Side Rendering
- OWASP: Application Security Verification Standard 5.0
- OWASP: DOM based XSS Prevention Cheat Sheet
- CISA: Known Exploited Vulnerabilities Catalog
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