Application Security

Vue.js Security Best Practices

Securing Vue.js applications from template injection, XSS through v-html, and third-party plugin risks.

Yukti Singhal
Security Engineer
4 min read

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:

  1. Check the package's maintenance status and contributors.
  2. Review its source code, especially the install() function.
  3. Check for known vulnerabilities in npm audit.
  4. 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.