We were recently contacted by one of our readers asking about a security vulnerability in one of their Meteor applications.

They noticed that when they weren’t authenticated, they were able to pull down a large number of documents from a collection through a publication they thought was protected.

The Vulnerability

The publication in question looked something like this:


Meteor.publish("documents", function() {
  return Documents.find({ 
    $or: [
      { userId: this.userId },
      { sharedWith: this.userId }
    ]
  });
});

When an unauthenticated user subscribes to this publication, their userId would be undefined, or null when it’s translated into a MongoDB query.

This means that the query passed into Documents.find would look something like this:


{
  $or: [
    { userId: null },
    { sharedWith: null }
  ]
}

The $or query operator means that if either of these sub-queries match a document, that document will be returned to the client.

The first sub-query, { userId: null }, mostly likely won’t match any documents. It’s unlikely that a Document will be created without a userId, so there will be no Documents with a null or nonexistent userId field.

The second sub-query is more interesting. Due to a quirk with how MongoDB handles null equality queries, the { sharedWith: null } sub-query will return all documents who’s sharedWith field is either null, or unset. This means the query will return all unshared documents.

An un-authenticated user subscribing to the "documents" publication would be able to view huge amounts of private data.

This is a bad thing.

Fixing the Query

There are several ways we can fix this publication. The most straight-forward is to simply deny unauthenticated users access:


Meteor.publish("documents", function() {
  if (!this.userId) {
    throw new Meteor.Error("permission-denied");
  }
  ...
});

Another fix would be to conditionally append the sharedWith sub-query if the user is authenticated:


Meteor.publish("documents", function() {
  let query = {
    $or: [{ userId: this.userId }]
  };
  if (this.userId) {
    query.$or.push({ sharedWith: this.userId });
  }
  return Documents.find(query);
});

This will only add the { sharedWith: this.userId } sub-query if this.userId resolves to a truthy value.

Final Thoughts

This vulnerability represents a larger misunderstanding about MongoDB queries in general. Queries searching for a field equal to null will match on all documents who’s field in question equals null, or who don’t have a value for this field.

For example, this query: { imaginaryField: null } will match on all documents in a collection, unless they have a value in the imaginaryField field that is not equal to null.

This is a very subtle, but very dangerous edge case when it comes to writing MongoDB queries. Be sure to keep it in mind when designing the MongoDB queries used in your Meteor publications and methods.