Earlier this year, I gave a talk at the local Meteor Chicago meetup titled “Meteor Security Essentials.” While I did release a slide deck and GitHub repo for the talk, I never really formalized my thinking into a post. Today, we’re going to extract and expand on some of the ideas in that talk, adding some new lessons learned over the last 7-8 months. The goal of this post is to give you some insight into what sorts of things to consider in respect to security and Meteor, how to handle them, and what you need to do to implement each technique. If you’ve already read the deck (or were present for the talk), consider most of this a refresher.
Ready? Let’s get started by talking about packages!
Packages
Arguably one of the best features of Meteor, packages allow us to quickly and easily snap functionality in-and-out of our applications, relying on the generous efforts of the community. When it comes to security, we need to keep a few things in mind about which packages are okay to keep installed, which can harm us, and which we should add to boost security.
Removing autpublish and insecure
When we first create a new Meteor project using the meteor create <my-project-name>
command, we’ll see two packages automatically installed for us: autopublish
and insecure
. Together, these two packages make working with Meteor out-of-the-box quick and easy. They are, however, only intended for prototyping. Any project that goes beyond the tinkering stage needs to remove both the autopublish
and insecure
packages. Why?
This requires an understanding of what these packages do. First, autopublish
is pretty descriptive: it automatically publishes all collections and all data to the client. Obviously this is not good. When we’re just getting started on a project locally this is fine, but for any project that intends to be put into production, you should remove autopublish
. In the off chance that none of the data in your database is sensitive you can leave this, but for 99% of applications, make sure to uninstall this early.
In the same regard, insecure
achieves a similar result to autopublish
in that it allows full write access on your database if you do not have any allow or deny rules written. To make this clear, here’s a quick GIF of the behavior of insecure
with and without allow/deny rules:
While allow/deny rules can save us, it’s still best to remove this package early on in your project. If you forget to write allow/deny rules (or the one’s you do write are incorrect), insecure
can end up biting you from behind.
Check and audit-arguments-check
To further boost security, two packages we should add to our applications are check
and audit-argument-checks
. Combined, these two packages:
- Allow us to write statements (
check
) inside of our Meteor Methods and Publications that evaluate data sent from the client to the server. - Alert us when we forget to write those statements (
audit-argument-checks
).
In Meteor, whenever we send something to the server from the client, it’s good to check
or verify that what we receive on the server is exactly what we expect. Consider the following pattern:
Client
let document = {
title: "Why you should eat pizza like I say",
url: 'https://medium.com/professional-eating/why-you-should...',
author: 'Roger Klotz',
date: 'October 26th, 2015'
};
Meteor.call( 'insertDocument', document, ( error ) => {
if ( error ) {
alert( error.reason );
}
});
Simple enough. Here, we’re making a call to a server-side Method called insertDocument
. The part we want to pay attention to is the object we’re declaring at our document
variable. Notice the explicit structure of the data. It has a title
, url
, author
, and date
field. When we send this to the server, our goal is to immediately insert this document into our database. Let’s take a look:
Server
Meteor.methods({
insertDocument( document ) {
Documents.insert( document );
}
});
See what’s happening here? Even though we’re passing our data to the server first (instead of handling inserts on the client), we’re still allowing any data to be inserted into our Documents
collection. For example, updating our client-side snippet:
Client
let document = {
title: 'Why you should eat pizza like I say',
url: 'https://medium.com/professional-eating/why-you-should...',
author: 'Roger Klotz',
date: 'October 26th, 2015',
blarghnaghgalll: 'junk daaaaaataatatatatatatata'
};
Meteor.call( 'insertDocument', document, ( error ) => {
if ( error ) {
alert( error.reason );
}
});
Hmm…that blarghnaghgalll
part looks a bit suspicious. But guess what? This will get inserted into our database just fine when we call our insertDocument
method. You may be thinking “okay, but I would never pass a value like that in my own code, so who cares?” Good question! This actually isn’t entirely about what we’ll do, but what others will do. Remember, because we’re invoking calls to our Methods on the client, technically speaking, anybody can inspect our client-side JavaScript and figure out the names of our methods.
As a result, if someone found insertDocument
, they might say “hmm, I’m going to guess this inserts a document into the database” and start inserting a bunch of junk. Now, how malicious that junk is depends on the rest of your security. Of course, though, we don’t want anybody to have the ability to do this. This is where check()
comes in. Let’s update our Method code and see what happens when we call this.
Server
Meteor.methods({
insertDocument( document ) {
check( document, {
title: String,
url: String,
author: String,
date: String
});
Documents.insert( document );
}
});
Nothing too wild. Here, when we call check( document, ... )
what we’re doing is telling the check()
package to verify that the document it receives from the client (passed as the first argument) matches the pattern we give it in the second argument. Two things are happening here: first, the check
will look at the fields in the document we give it, but also, the types of those fields. Here, we expect to have four fields, each with a type of String
. If this check()
were to receive our “bad” document example, it would immediately throw an error, preventing the Method from completing. Make sense?
Using the check() package, we can prevent any unwanted data from getting into our database. This doesn’t only guard us against attackers, it also helps us to guard us against ourselves. Consider a feature in your application where you insert documents into the Documents
collection in two different ways. Using check()
you can prevent accidentally adding field names or data types that don’t match other documents.
Of course, remembering to do this can be a bit overwhelming, so adding the audit-argument-checks
package can automate this process. Whenever we define a new method or publication without a check()
statement, we’ll get an error in our terminal. Helpful!
Browser policy
Something that we may not think about when we’re building our applications is how attackers may “piggyback” on our code to make it do things we didn’t intend. Better known by names like Cross-site scripting or Clickjacking, these malicious techniques allow attackers to inject unwanted code and functionality into our applications, tricking our users. To guard against this, we can add the browser-policy
package which allows us to define explicit rules about what types of content can be loaded in our applications and by whom.
Out-of-the-box, the browser-policy
package sets us up with a generic policy that helps us to prevent some common issues like unwanted <iframe></iframe>
elements from being loaded, only allowing websites on the same origin as our own (e.g. themeteorchef.com
) to frame our application, disallowing usage of JavaScript’s eval()
method, etc. What’s nice about this is that it gives us a decent layer of security without much thinking. Of course, when we do this, we can run into situations where something we don’t want to be blocked is blocked from loading.
Consider the following example. We want to add a YouTube video to our page, so we add the iframe
we get from YouTube. Without browser-policy
, here’s what we get:
And when we add browser-policy
:
This may seem a bit annoying, but consider a situation where our user visits a malicious site that installs some unwanted code in their browser without their knowledge. That code is designed to attach itself to any insecure website. When they visit our application (without browser-policy
), that code wraps our page in a frame. Now, instead of data being sent to our own server, the code is designed to re-route data to the attacker’s server. Yikes! Using browser-policy
, we can prevent this by blocking anything from loading on the page (or framing our application) that we don’t explicitly allow.
In the example above, if we want to keep browser-policy
installed and allow our YouTube video to load, we can add the following on the server:
Server
BrowserPolicy.content.allowOriginForAll( '*.youtube.com' );
This translates to allowing any content from the domain youtube.com
(a trusted source) to load on our page. If for some reason an attacker attempted to insert a link from youtabe.com
, it would be blocked like the example above. Pretty neat, right? Now, when we visit our application, our YouTube video will reappear and other unwanted sources will still be blocked.
Database
One of the most—if not the most—sensitive places in our application is our database as it’s responsible for storing all of our users data. While it can be argued that not every application stores “sensitive data,” this definition can be interpreted differently by a lot of people. A favorite example of mine comes from the team over at Basecamp. In 2011, they wrote a post about the growth of their product which included a fun little anecdote that the 100 millionth file to be uploaded to Basecamp was a file called cat.jpg
.
The post was innocent enough, but it was quickly made clear to the Basecamp folks that sharing this information was not okay. Even though it was just a picture of a cat, Basecamp had unwillingly shared a piece of data that, despite being innocuous, was still a users personal data and not subject to being revealed to the public. The obvious question was “what if it was a picture of someone’s kid?” Spooky. Basecamp addressed this head on but the lesson for other developers was clear: consider everything in your database to be sensitive, even if you don’t think it is.
Preventing ourselves from leaking data is pretty easy, but what about our code? Here are a few things to consider when it comes to our databases in respect to security that should be buttoned up.
Disabling allow and deny
I, along with others, feel that allow/deny rules are a tricky sort. They work well, but in order for them to work well, the developer needs to understand and implement them correctly. As it was clearly demonstrated in Discover Meteor’s Allow & Deny Security Challenge, goofing up allow and deny rules is pretty easy to do.
As a result, my personal preference is to remove allow and deny rules from the equation entirely, forcing method-only database operations. This means that calling things like Documents.insert()
from the client is disabled by default. Why? While it does add some work to our plate to wire up a Method for every database operation, it also protects us from unwanted database operations being performed on the client. To “disable” allow and deny rules, we need to make sure to write rules for each of the collections in our application that explicitly prevent any client-side operations. An example using our Documents
collection from earlier:
Client & Server (e.g. /both directory)
Documents = new Meteor.Collection( 'documents' );
Documents.allow({
insert() { return false; },
update() { return false; },
remove() { return false; }
});
Documents.deny({
insert() { return true; },
update() { return true; },
remove() { return true; }
});
Here, we’re defining both allow
and deny
rules for our Documents
collection. We use both because technically deny
rules will override allow
rules. Setting both ensures that they’re blocked 100%.
When a client tries to write to a collection, the Meteor server first checks the collection’s
deny
rules. If none of them returntrue
then it checks the collection’sallow
rules. Meteor allows the write only if nodeny
rules returntrue
and at least oneallow
rule returnstrue
.
To clarify this, in the Documents.allow
block we’re saying “should we allow insert, update, and remove methods on the client? No (or false
).” In the Documents.deny
block we’re saying “should we deny insert, update, and remove methods on the client? Yes (or true
).” When we do this, if we attempt to call any insert
, update
, or remove
methods on our Documents
collection, they will fail.
It may not be entirely clear, but because allow
and deny
rules are vulnerable to mistakes, this guarantees that we don’t have any unwanted access to our database on the client. Again, it adds some work for us, but also allows us to sleep well at night.
Don’t forget users
In tandem with this practice of always disabling allow
and deny
rules is remembering to do the same with your Meteor.users
collection. Because this collection is defined for us when we install any of the accounts-related packages, it’s easy to forget that by default, this collection allows client-side related operations. To ensure that we don’t allow unwanted access to our users collection—arguably the most sensitive of all our collections—we need to add the same allow
and deny
rules to it:
Client & Server (e.g. /both directory)
Meteor.users.allow({
insert() { return false; },
update() { return false; },
remove() { return false; }
});
Meteor.users.deny({
insert() { return true; },
update() { return true; },
remove() { return true; }
});
Now, just like our own collections, we can ensure that Meteor.users
can only be modified on the server. Peace and quiet.
Using publish and subscribe
Once we’ve removed the autopublish
package from our application, it becomes necessary to explicitly define publications on the server and subscribe to those from the client. These patterns allow us to define exactly what data is sent to the client. Remember, on the client, Meteor relies on something called minimongo which acts as a client-side data store for our users. What’s unique about it—and often misunderstood—is that this collection only holds what we allow it to. Consider the following diagram:
Here, we can see that our publication takes the documents from the server, trims down the result based on our publication and then sends those documents to the client. What does this have to do with security? Well, depending on the situation, we may not want all of our documents to be available on the client. Taking the above example, what if our Documents
collection contained documents for every user in our application? If we published everything, then our users would be able to access each other’s documents. With a publication, we can control this on a more granular level, specifying exactly the documents that should be sent to the client.
So it’s clear, here’s this same example, relying on the current user’s ID to filter down the documents that get sent to the client:
Server
Meteor.publish( 'userDocuments', function() {
return Documents.find( { 'owner': this.userId } );
});
Here, we’re defining our publication so that only those documents owned by the current user are accessible on the client (i.e. to the current user). If this user logs out, our publication will return nothing! Perfect. This is pretty good, but we can actually go a bit further.
Overpublishing
One security problem that doesn’t seem like a security problem at first is overpublishing. Similar to our current user example above, we have the option to be very specific about what data we want to send to the client. This comes in handy for situations where we have a document that has some information that we don’t want the client to see. If we do a vanilla publication, that information gets sent down to the client. To avoid it, we can rely on MongoDB’s projections to specify not just what documents to return, but what fields on each of those documents should be returned. Consider the following document:
Database
{
title: "Document title",
owner: "Document owner",
contents: "Document contents",
secureField: "Some secure data"
}
What we want to accomplish is returning this document with the title
and contents
fields only, leaving out owner
and secureField
. Here’s how we might do it (avoiding an overpublication):
Server
Meteor.publish( 'documents', function() {
return Documents.find( {}, { fields: { title: 1, contents: 1 } } );
});
On the client, now, if we called Documents.findOne()
, we’d see a document like the following:
Client (minimongo)
{
title: "Document title",
contents: "Document contents"
}
Neat! Simply by passing { fields: { title: 1, contents: 1 } }
to our collection’s .find()
method, we can control which fields get sent down to the client. Here, by specifying title
and contents
followed by a 1
(where 1
is equal to true
), we’re saying “for each document, only return these fields and leave everything else out.” To make this clear, we could inverse this and only get back the owner
and secureField
fields on the client:
Server
Meteor.publish( 'documents', function() {
return Documents.find( {}, { fields: { title: 0, contents: 0 } } );
});
By setting our fields
values to 0
we’re saying “do not return these fields, but return everything else.” Depending on how you want to write your publications, you could easily update this to read as { fields: { owner: 0, secureField: 0 } }
and get the same result as our first example. Pretty handy.
Accounts
An important feature to consider in any application is the addition of an accounts system. Fortunately for us, Meteor makes this realtively easy with the addition of packages like accounts-ui or community packages like user-accounts. Of course, we also have the option to roll our own authentication so there are a few other things to keep in mind.
Accounts._hashPassword()
If we’re making calls to Accounts.createUser
on the server (e.g. we pass our users signup information to the server first), we want to make sure that our users password is properly hashed before we send it over the wire. To do this, we can rely on Meteor’s private Accounts._hashPassword()
method to ensure that whatever we send is protected.
Client
let user = {
email: template.find( '[name="email"]' ).value,
password: Accounts._hashPassword( template.find( '[name="password"]' ).value );
};
Meteor.call( 'createAccount', user, ( error ) => {
if ( error ) {
alert( error.reason );
}
});
What does this do? This ensures that if we pass a user’s password from the client to the server, instead of being in plain text, it’s sent as a sha-256
hash value. To make this a bit more clear:
Example Usage of Accounts._hashPassword()
Accounts._hashPassword( 'tacos123' );
Object {digest: "6183c9c42758fa0e16509b384e2c92c8a21263afa49e057609e3a7fb0e8e5ebb", algorithm: "sha-256"}
Instead of sending tacos123
over the wire—which could potentially be hijacked—instead, we send a cryptographic hash in the form of an object with a key digest
that needs to be evaluated against another hash only available on the server (handled by Meteor behind the scenes). What’s neat about this is that this is exactly how Meteor’s Accounts.createUser() function works on the client. Okay…so why would we need this? Well, consider an application where you wanted to do some other stuff (e.g. process a payment) on the server before creating an account for a user.
To make all of this work, we need to pass the user’s credentials to the server and once our other work is complete, then create their account. Using Accounts._hashPassword()
we can send their password up to the server without running the risk of it being exposed to an attacker.
Disabling client-side accounts
In tandem with this, another option we have is to disable client-side account creation via Accounts.createUser()
entirely. Generally speaking, you should only need to do this if you have a custom signup flow that requires steps on the server first before creating a user. Otherwise, you will simply end up needing to reimplement the Accounts.createUser()
flow using a method which does the same thing (i.e. redoing work). If you’re doing the former, though:
Server
Accounts.config({
forbidClientAccountCreation: true
});
With this in place, if we attempt to call Accounts.createUser()
from the client, it will be rejected. Again, think carefully about whether or not you really need to do this before implementing it. It’s great if you have a custom signup flow, but unnecessary if you’re ust relying on the existing accounts packages to handle signups.
Roles
When it comes to applications with multiple users, adding the alanning:roles package can be invaluable. The more popular of the options for implementing user roles, using this package, we can add different types of users to our application (e.g. admin
vs. client
) and control what each group has access to in the application. When it comes to security, this is handy on a user-to-user basis as it gives us the ability to control who can see what and when. A simplified example of this is using the isInRole
template helper to control what gets renderd for users in a template:
Client
<template name="profile">
{{#if isInRole 'admin'}}
<button class="delete-user">Delete User</button>
{{/if}}
<h3>{{user.name}}</h3>
<p>{{user.email}}</p>
</template>
Here, we see the usage of roles to allow only those users in the admin
group to get access to the “Delete User” button. If a user were in a group like client
instead, they wouldn’t see the button rendered. While managing access in the template is neat, we can go even further and do things like secure our publications using roles. Using the alanning:roles
package, we gain access to a full API for evaluating user roles to bolster our security.
Using settings files
If we’re building a non-trivial application in Meteor, it’s likely that at some point, we’ll have to add support for third-party services. To make use of these services, we’ll often have to store API keys and access tokens that allow us to interact with these services. One option we have is to store this information directly in our source code which we do not want to do. Why? All it takes is our source code getting leaked once and we’re immediately open to risk. This could happen because a service we host our code with has a security failure of their own, or, we—or someone on our team—goofs up and lets our source into the wild.
To solve this problem, we can make use of settings files in Meteor. These files allow us to keep a collection of both public and private values that we want to store in our application using the JSON
format. When we’re building our applications, then, instead of embedding our API keys directly in our source code like this:
Server
let GitHubAPI = new GitHub( '1a5578ghju7181501511' );
We can store the values in a settings file and retrieve them like this:
Server
let GitHubAPI = new GitHub( Meteor.settings.private.github );
The difference? If our source code is leaked, the person who gets access to it has no knowledge of our API keys or access tokens. Going even further, we can create multiple settings files in our application that contain values for different environments. For example:
settings-development.json
{
"github": "1a5578ghju7181501511"
}
Here, we have a file called settings-development.json
where our value for github
represents an API key that we’ve assigned to our development environment. For our production environment:
settings-production.json
{
"github": "125498bajk718912a642"
}
See what’s happening here? In our settings-production.json
file, we have a completely different API key. The point here is that with Meteor, we can tell our server to boot with a specific settings file, meaning, when we deploy our application, we can specify usage of our settings-production.json
file. What this gets us in turn is that our code will work just fine (remember, we’re just pointing to a generic Meteor.settings.private.github
value). If we had hardcoded values, we’d run into a situation where if we wanted to use separate keys for each environment, we’d have to write a lot of unnecessary—and unsafe—logic to make it work.
This is an incredibly important topic, so make sure to read this snippet which goes into detail on how to handle this per environment.
SSL
As it’s explained in this alarming post by Pete Corey over at East5th, running a Meteor application without SSL configured can make your application immediately vulnerable. This is much like any web application and how it communicates with the server. Specific to Meteor and its usage of DDP, it’s easy to “sniff” the traffic being sent to the server and inspect it. The result? A clever attacker could easily grab the login credentials for a user and login as them without issue. Of course, this is just one of several issues that running an application without SSL presents.
With a Meteor application, setting up SSL requires two parts: first, purchasing and installing an SSL certificate on your production server and second, adding the force-ssl
package to your application. The latter step, adding the force-ssl
package, ensures that all connections to your application are made over https://
. This means that if a user were to visit your application at http://app.com
, Meteor would detect this and automatically redirect them to https://app.com
to ensure that their traffic is handled over a secure connection.
Where to get an SSL certificate?
This depends on the needs of your project and the type of certificate you want. My personal go to is Namecheap which allow you to purchase different grades of certificates from a handful of different providers.
Wrapping Up
This completes a basic look at security in Meteor applications. When we’re developing applications, it’s important to keep all of these things in mind, while also considering how they might change based on our application. Never skimp on security. It’s easy to say “I’ll take care of it later” early on in a project, only to never revisit it again leaving your application open to all sorts of unwanted activity. Be safe!
How do you practice security? Did I miss something here that you’ve found helpful in keeping everything locked up tight? Share your thoughts and tips in the comments!