Vulnerability
Vue

XSS in Vue.js Applications

Vue.js auto-escapes text interpolation in templates ({{ }}), but the v-html directive renders raw HTML without any sanitization. Combined with dynamic attribute binding and server-side rendering pitfalls, Vue apps face multiple XSS attack surfaces that require deliberate mitigation.

Scan Your Vue App

How XSS Manifests in Vue

The primary XSS vector in Vue is the v-html directive, which directly inserts raw HTML into the DOM. Developers commonly use it for rendering rich text content, markdown previews, and email templates. Dynamic attribute binding with v-bind:href or :href allows javascript: URL injection. Unlike text interpolation, attribute binding does not prevent malicious protocols. Vue's template compiler can be exploited in rare cases where user input is used to dynamically construct template strings at runtime. Server-side rendering with Nuxt or Vue SSR can also introduce XSS if data is not properly escaped before being serialized into the initial HTML. Custom directives that manipulate the DOM (el.innerHTML in directive hooks) are another common source of XSS in Vue applications.

Real-World Impact

A Vue.js forum application used v-html to render formatted posts with user-generated content. An attacker submitted a post containing an SVG element with an onload handler that exfiltrated session tokens. The payload persisted in the database (stored XSS), affecting every user who viewed the thread. In another incident, a Vue admin panel used dynamic :href bindings for user-provided profile links without validation. Attackers injected javascript: URLs that executed admin-level API calls when clicked.

Step-by-Step Fix

1

Sanitize v-html content

Create a global sanitization composable or directive to clean HTML before rendering.

// composables/useSanitize.ts
import DOMPurify from 'dompurify';

export function useSanitize() {
  function sanitize(html: string): string {
    return DOMPurify.sanitize(html, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
      ALLOWED_ATTR: ['href', 'target', 'rel'],
    });
  }
  return { sanitize };
}

// In component
<template>
  <div v-html="sanitizedContent" />
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useSanitize } from '@/composables/useSanitize';
const { sanitize } = useSanitize();
const props = defineProps<{ content: string }>();
const sanitizedContent = computed(() => sanitize(props.content));
</script>
2

Create a safe href directive

Build a custom directive that validates URLs before binding to href.

// directives/safe-href.ts
import type { Directive } from 'vue';

const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:'];

export const vSafeHref: Directive<HTMLAnchorElement, string> = {
  mounted(el, binding) {
    updateHref(el, binding.value);
  },
  updated(el, binding) {
    updateHref(el, binding.value);
  },
};

function updateHref(el: HTMLAnchorElement, url: string) {
  try {
    const parsed = new URL(url);
    el.href = ALLOWED_PROTOCOLS.includes(parsed.protocol) ? url : '#';
  } catch {
    el.href = url.startsWith('/') ? url : '#';
  }
}
3

Replace v-html with component-based rendering

Use a Vue markdown component instead of rendering raw HTML.

<!-- UNSAFE -->
<div v-html="userMarkdown" />

<!-- SAFE -->
<template>
  <vue-markdown :source="userMarkdown" />
</template>

<script setup>
import VueMarkdown from 'vue-markdown-render';
const props = defineProps<{ userMarkdown: string }>();
</script>
4

Add CSP headers

Configure your server or hosting platform to send Content Security Policy headers.

// vite.config.ts (for dev server)
export default defineConfig({
  server: {
    headers: {
      'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
    },
  },
});

Prevention Best Practices

1. Avoid v-html with any user-controlled content. Use text interpolation {{ }} which auto-escapes by default. 2. If v-html is necessary, sanitize content with DOMPurify before binding. 3. Validate all URLs in :href bindings. Block javascript: and data: protocols. 4. Never construct Vue templates from user input at runtime. 5. In custom directives, never use el.innerHTML with untrusted data. 6. Configure CSP headers to block inline script execution.

How to Test

1. Search your codebase for v-html directives: grep -r "v-html" --include="*.vue" 2. Test each v-html usage by providing <img src=x onerror=alert(1)> as input. 3. Find all :href or v-bind:href bindings and test with javascript:alert(1). 4. Check custom directives for el.innerHTML usage. 5. Use Vibe App Scanner to automatically detect XSS patterns in your Vue application.

Frequently Asked Questions

Does Vue automatically prevent XSS?

Vue auto-escapes text interpolation in double curly braces {{ }}, which prevents XSS for text content. However, v-html bypasses this protection entirely, and dynamic attribute bindings like :href do not block javascript: URLs. You must sanitize v-html content and validate URLs manually.

Is v-html safe to use in Vue?

v-html is safe only when the content comes from a fully trusted source and has been sanitized with a library like DOMPurify. Never use v-html with user-generated content without sanitization. Consider using a markdown rendering component as a safer alternative.

How does XSS in Vue differ from React?

Both frameworks auto-escape text by default. The key difference is Vue's v-html directive vs React's dangerouslySetInnerHTML - functionally similar, but v-html is more commonly used in Vue codebases because of its simpler syntax. Vue also has custom directives that can manipulate the DOM unsafely.

Is Your Vue App Vulnerable to XSS?

VAS automatically scans for xss vulnerabilities in Vue applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Vue stack.