Vue's template compiler escapes interpolated content by default, so {{ userInput }} renders a script payload as harmless text. That covers the common case beautifully — and it lulls teams into thinking Vue is XSS-proof. It is not. Vue hands you a small set of deliberately unescaped escape hatches, and every real Vue XSS I have reviewed traces back to one of them: the v-html directive, user-controlled attribute bindings, dynamic component resolution, or hydration mismatches in server-side rendering. This guide covers where Vue's automatic escaping ends and the patterns that keep the escape hatches from becoming attacker entry points.
Where does Vue's automatic escaping actually apply?
To text. Mustache interpolation ({{ }}) and v-text always render values as plain text, HTML-escaped, never as markup. Attribute bindings via v-bind (:href, :src, :style) are not HTML-injection points in the same way, but they carry their own risks — a :href bound to javascript:... still executes. The one directive that fully opts out of escaping is v-html.
Why is v-html the directive to audit first?
Because v-html sets innerHTML directly. Whatever string you give it is parsed as HTML and inserted into the DOM with no sanitization. Vue's own documentation is blunt about it: only use v-html on trusted content, never on anything a user can influence.
<!-- DANGEROUS if comment is user-controlled -->
<div v-html="comment"></div>
If you must render user-authored HTML — comments, markdown, CMS bodies — sanitize before binding:
import DOMPurify from "dompurify";
export default {
computed: {
safeComment() {
return DOMPurify.sanitize(this.comment);
},
},
};
<div v-html="safeComment"></div>
Sanitize at render time in a computed property, not once at storage — your allowlist can change between the two.
How do attribute and URL bindings go wrong?
Two ways. First, URL schemes: a :href or :src bound to attacker-controlled input like javascript:alert(document.cookie) runs on click. Allowlist schemes before binding:
function safeUrl(url) {
try {
const u = new URL(url, window.location.origin);
return ["http:", "https:", "mailto:"].includes(u.protocol) ? url : "#";
} catch {
return "#";
}
}
Second, dynamically binding an entire attribute object with v-bind="userObject" lets a user inject arbitrary attributes — including event handlers or innerHTML-adjacent ones — so never spread an untrusted object into v-bind. The same caution extends to :style bindings built from user input: CSS can smuggle behavior through properties and url() references, so bind only individual, validated style values rather than an arbitrary user-supplied style string.
Is dynamic component resolution a risk?
It can be. <component :is="name"> resolves a component by name. If name comes from user input and your app resolves component names loosely, an attacker may be able to steer rendering toward an unintended component, or in some setups toward raw HTML element names that carry their own behavior. Keep the set of resolvable components to an explicit allowlist rather than passing user input straight into :is.
What is different about security in server-side rendering?
SSR (Nuxt or a custom Vue SSR setup) renders components to an HTML string on the server, then hydrates on the client. Two things change. First, v-html content is now rendered server-side into the initial HTML — unsanitized input becomes stored XSS delivered in the first byte, before any client script runs. Second, state serialized from server to client (the hydration payload) must be treated as a trust boundary: only serialize fields the client needs, never internal or sensitive data. Escape and sanitize on the server exactly as rigorously as on the client.
Do secrets and config leak through a Vue build?
Yes, if you put them there. Anything referenced through Vite's import.meta.env with the VITE_ prefix (or the Vue CLI's VUE_APP_ prefix) is inlined into the client bundle at build time and is fully readable in the browser. That is fine for a public API base URL, but a private API key, a signing secret, or an internal-only endpoint must never carry those prefixes — keep real credentials on a backend the Vue app talks to. Assume an attacker has read your entire compiled bundle, because opening DevTools is all it takes.
What HTTP-level defenses should a Vue app add?
A strict Content-Security-Policy is the backstop for any v-html slip — script-src 'self' with nonces blocks inline injection even if sanitization is missed. Add X-Content-Type-Options: nosniff, HSTS, and a sensible Referrer-Policy. If your app uses cookie-based sessions, protect state-changing requests with anti-CSRF tokens and SameSite cookies. Because these are runtime behaviors, confirm them with a dynamic scan against the deployed app rather than trusting the config alone.
Vue security checklist
- Default to
{{ }}/v-text; they always escape. - Treat every
v-htmlas an HTML injection point until sanitized with DOMPurify. - Allowlist URL schemes on
:hrefand:src. - Never spread untrusted objects into
v-bind. - Restrict
<component :is>to an explicit allowlist. - In SSR, sanitize server-side and only serialize client-needed state.
- Enforce a strict, nonce-based CSP.
- Run continuous SCA on the dependency tree.
Don't overlook the dependency tree
Vue apps depend on npm like any JavaScript project, and 2025 proved that tree is under active attack — the Shai-Hulud worm hit 500+ packages (CISA, September 2025) and trojanized chalk and debug reached wide distribution. Framework hygiene can't stop a poisoned transitive dependency, so pair it with software composition analysis on the full graph.
How Safeguard Helps
Safeguard resolves your entire Vue dependency tree, flags malicious and typosquatted packages before they hit a build, and uses reachability analysis to focus you on the CVEs your code actually exercises. Griffin, our AI engine, reviews new versions for the behavioral patterns behind the 2025 npm compromises, and auto-fix opens tested pull requests with the minimal safe upgrade. Sanitizing v-html, allowlisting schemes, and locking down SSR serialization stay your job — Safeguard secures the dependencies underneath.
Secure your Vue project's supply chain — create a free account, read the documentation, or compare Safeguard to other scanners.