Vue.js has excellent default security posture. Its template syntax escapes HTML by default, its reactivity system avoids direct DOM manipulation, and its single-file component model keeps things organized. But every framework has escape hatches, and Vue is no exception. This guide covers where Vue's security model is strong and where you need to be careful.
Vue's Default Protections
Automatic HTML Escaping
Vue's template interpolation escapes HTML entities automatically:
<template>
<p>{{ userInput }}</p>
</template>
If userInput contains <script>alert('xss')</script>, Vue renders it as plain text. The double-curly-brace syntax always escapes. This is the same protection React provides with JSX.
Template Compilation
Vue templates are compiled into render functions at build time (with the Vue compiler or Vite). This means template injection attacks, where an attacker provides a malicious template string, are not possible with precompiled templates.
Dangerous Patterns
v-html
The v-html directive renders raw HTML without escaping:
<template>
<!-- DANGEROUS: renders raw HTML -->
<div v-html="userContent"></div>
</template>
This is Vue's equivalent of React's dangerouslySetInnerHTML. If userContent comes from user input, you have an XSS vulnerability.
Mitigation: Sanitize before rendering:
<script setup>
import DOMPurify from 'dompurify';
import { computed } from 'vue';
const props = defineProps({ rawHtml: String });
const sanitizedHtml = computed(() => {
return DOMPurify.sanitize(props.rawHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target'],
});
});
</script>
<template>
<div v-html="sanitizedHtml"></div>
</template>
Dynamic Component Rendering
Vue's <component :is="..."> can render arbitrary components. If the component name comes from user input, an attacker could potentially render unintended components:
<!-- Potentially dangerous if componentName is user-controlled -->
<component :is="componentName" />
Always validate component names against an allowlist:
<script setup>
const allowedComponents = {
'user-profile': UserProfile,
'settings-panel': SettingsPanel,
};
const resolvedComponent = computed(() => {
return allowedComponents[props.componentName] || null;
});
</script>
URL Binding
Like React, Vue does not sanitize URLs in bindings:
<!-- Vulnerable to javascript: protocol -->
<a :href="userUrl">Click here</a>
Validate URLs:
<script setup>
function sanitizeUrl(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol) ? url : '#';
} catch {
return '#';
}
}
</script>
<template>
<a :href="sanitizeUrl(userUrl)">Click here</a>
</template>
Server-Side Rendering with Nuxt
Nuxt.js (the Vue equivalent of Next.js) introduces server-side rendering, which brings the same serialization risks:
<!-- Be careful with data serialized for hydration -->
<script>
export default {
async asyncData() {
// This data is serialized into the HTML page
// Never include secrets here
return { publicData: await fetchPublicData() };
}
};
</script>
Nuxt 3's server routes (server/api/) are actual backend endpoints. Apply the same security practices as any API: validate input, authenticate requests, rate limit.
Vue Plugin Security
Vue's plugin system allows third-party code to modify the Vue instance globally:
app.use(somePlugin);
A malicious or compromised plugin has access to:
- The global Vue application instance
- All component instances
- The router
- The store (Pinia/Vuex)
- Custom directives
Before installing a Vue plugin:
- Check the package's maintenance status and contributors.
- Review its source code, especially the
install()function. - Check for known vulnerabilities in
npm audit. - Assess its dependency tree.
State Management Security
If you use Pinia or Vuex, the same rules apply as with any client-side state management:
- Never store secrets in the store.
- Assume all client-side state can be inspected and modified by the user.
- Use Vue DevTools protection in production (devtools are disabled in production builds by default).
// Pinia store: don't store sensitive data
const useAuthStore = defineStore('auth', {
state: () => ({
isAuthenticated: false,
userName: '',
// DON'T: token: '', apiKey: ''
}),
});
Content Security Policy for Vue
Vue 3's production build does not use eval() or new Function(), so you can set a strict CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.yourapp.com;
If you use Vue's runtime compiler (which most production apps do not), you will need 'unsafe-eval'. Avoid this by using precompiled templates.
Dependency Security
Vue projects use npm or yarn. Keep Vue and its ecosystem packages updated:
# Check for updates
npm outdated
# Audit for vulnerabilities
npm audit
# Generate SBOM
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
How Safeguard.sh Helps
Safeguard.sh monitors your Vue.js application dependencies continuously. It tracks the Vue framework version, Nuxt version, Pinia, Vue Router, and every other package in your tree. When vulnerabilities are disclosed in the Vue ecosystem, Safeguard.sh maps the impact to your specific applications and helps your team prioritize updates. For organizations with multiple Vue apps, it provides the consolidated supply chain visibility that keeps you ahead of emerging risks.