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.