DEV Community

Nando Delgado
Nando Delgado

Posted on • Updated on

Should you care about XSS in Vue.js?

Let’s get the obvious part of this article out of the way first: if you don’t sanitize your data you’ll always be vulnerable to cross-site scripting (XSS) attacks, no matter what framework you use.

The goal of this article is to show you a few ways that you might become vulnerable to XSS while using Vue, and hopefully, how to prevent them.

If at this point you’re thinking “wait, what’s cross-site scripting?”, then we need to backtrack a little bit. If you’re already familiar with this subject, then you can skip the next section right on through.

What is cross-site scripting?

Cross-site scripting (XSS) is a type of web app vulnerability that injects client-side scripts into pages viewed by other users.

XSS is caused when sites render user input directly into a page without processing (sanitizing) it first by escaping special characters. This enables attackers to add scripts through regular user inputs, or URL parameters, that will then be executed once the page loads.

You can read more about two types of XSS here: Reflected XSS and Stored XSS

So how is Vue vulnerable?

Any time server-generated HTML is injected into a website, that website may be vulnerable to XSS attacks. In the case of Vue this is through the v-html directive.

The v-html directive

The v-html directive in Vue is used to output raw HTML into a component in your app.

To be honest, there’s probably few good reasons to use this if you’re already using Vue, as you should be able to apply any attributes dynamically.

However, one use case as mentioned in would be if you’re working with a legacy system that has raw HTML stored in a database and you need to render that in your app.

So unlike using mustache expressions, and although v-html might be useful (it’s there for a reason after all), it can open you up to XSS attacks since Javascript rendered through v-html will be executed.

See a quick demo here of the same string rendered via mustache expressions and v-html and try and click on the link there to see the injection in action.

Dynamically rendering arbitrary HTML on your website can be very dangerous because it can easily lead to XSS vulnerabilities. Only use HTML interpolation on trusted content and never on user-provided content.

Mixing server-side and client-side rendering

Another time sites using Vue may be vulnerable to XSS is if they mix server-side and client-side rendering, even if you are escaping characters. What’s also worth noting is that this vulnerability applies even if you’re not using v-html.

This is explored in detail in this repo by dotboris, it includes a very clear example and instructions which we will overview below.

To briefly run through this case, the app takes a user input as a query parameter and renders it. Both the input field and the rendered HTML are in a Vue element that is used to dynamically increase or decrease the number in a counter.

If we write an expression with a bit math in the input field you’ll see that it’s correctly processed by Vue, for example, typing

{{ 2 + 2 }}

in it will result in the app rendering

You have injected: 4

So now we know that an injection can be done.

Using that same method for any Javascript function though, won’t work as well. So if we try

{{ alert(‘xss’) }}

we’ll get something like:

TypeError: alert is not a function
at Proxy.eval (eval at createFunction (vue.js:10518), <anonymous>:3:114)
at Vue$3.Vue._render (vue.js:4465)
at Vue$3.updateComponent (vue.js:2765)
at Watcher.get (vue.js:3113)
at new Watcher (vue.js:3102)
at mountComponent (vue.js:2772)
at Vue$3.$mount (vue.js:8416)
at Vue$3.$mount (vue.js:10777)
at Vue$3.Vue._init (vue.js:4557)
at new Vue$3 (vue.js:4646)

This is because any Vue expressions are evaluated in the context of their instance. So when we typed alert(‘xss’) it tried looking for the alert method in our Vue instance, which of course, does not exist.

To go around it, the example in the repo goes with

{{constructor.constructor("alert('xss')")() }}

If you try typing that into the input field you should see it work without a problem.

Why does this work? Quoting directly from dotboris:

“In javascript, all constructors are functions and all functions are objects. This means that Vue$3 has a constructor. This constructor is the Function constructor. Writing constructor.constructor gives us the Function constructor.

The Function constructor let’s us define a function dynamically at runtime. We pass it the code of our function and it returns a function that we can run. In this case we end up with Function(“alert(‘xss’)”)(). This creates a function that calls alert (the real alert in the global scope) and then calls it.”

This works because although the user input is being escaped by the app, when the page gets to the browser Vue takes the HTML and renders it like a template, running a complex eval on that HTML.

At this point Vue can’t tell the difference between the template which is safe, and any unsafe input that may have been sent by the user.

So how can we prevent this? Using the v-pre directive whenever server-side values are injected into the template works well, but it’s easy to miss when it needs to be manually added into each and every element that does this.

An alternative proposed by the author of this example is to define a global variable in the page that holds all server side variables, that way $_GET[‘var’] would become SERVER_VARIABLES.var, which gives the developer a more secure way of passing values from the server to the client.

On our side, our recommendation would also be to limit Vue to where it’s needed in cases like this. One of the benefits of Vue vs other frameworks is that it doesn’t need to be used on a whole page.

In this particular example Vue is only used to increase and decrease the number in a counter, but the counter and the element that displays user input are inside the same div, and so are affected by the same Vue instance.

If instead of keeping it this way we take the element displaying user input and put it outside the Vue element, then the app doesn’t lose any functionality, and any user input rendered is safely displayed regardless if it’s a function or not (as long as it’s escaped server-side).

Long story short, always remember to escape user input, and as convenient as modern frameworks may be, don’t depend on them having covered every single security flaw.

V-pre and avoiding injecting raw HTML directly are good practices, but as it is with app, it’s better to take the time to understand where there may be holes in your app early on, and learn how to prevent them.

Discussion (8)

steeve profile image

I came to this article because I need to display raw HTML into a component in my app. The raw HTML is coming from vue-quill-editor and I display it with v-html. Unfortunately with the latest version of eslint, I'm getting the error: warning 'v-html' directive can lead to XSS attack vue/no-v-html.
Is there a better practice to render HTML without v-html with vuejs?
By the way, good article 👏

nandod1707 profile image
Nando Delgado Author

Hey Steeve! If you absolutely need to use v-html (which I understand you do), then you should look into sanitizing user input when it gets to the server. I can recommend this library I've been working with recently, if you're using Node server side then it might help!

steeve profile image

I will take a look, thanks Nando :)

phillygogo profile image

Hey Steeve. If you trust the data then you can happily use v-html. E.g. your data is coming from your own CMS

narviktribe profile image
EJ Santoemma • Edited on

This article is wrong, @nandod1707 you should change or remove it.

You thought that

{{ constructor.constructor("alert('xss')")() }}

is the same of

s = `constructor.constructor("alert('xss')")()`
{{ s }}

But the first opens the alert, the second outputs the string
With the first you are hacking yourself.

In a real app you are in the second scenario, where s is taken from the server, and Vue correctly shows it as a string.

Anyone can try, though: (spoiler: it show's the string)

  <div>{{ s }}</div>

export default {
  data() {
    return {
      s: `constructor.constructor("alert('xss')")()`
nandod1707 profile image
Nando Delgado Author

Hi EJ! Thanks for your comment. Sure, what you're saying is similar to what we suggest in the article of putting server side variables inside a global variable. The goal of the article is to point out how to avoid making an app vulnerable through bad practices, and your example seems to be in line with that :)

narviktribe profile image
EJ Santoemma

Hi Nando. The standard (and only) way to show a value coming from outside is to put it into a property (data, props, vuex, ...) and then show it in the template via the mustaches, v-text or v-html.

That global variable trick is a solution to a non problem.
Your "constructor" example had been discussed and should be a thing of the past.

I'm replying here because I found your article on Hacker News last week, and thought that it can generate FUD to those learning any reactive framework.

Like your hint to limit the use of Vue...

Peace :)

joeschr profile image

important article, thanks!