Should you care about XSS in Vue.js?

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.


Backed by

Hackmetrix startup chile