Mongoose 5.13.0 was released on June 26, 2021 and includes 17 new features, including 5 community contributions. Mongoose 5.13 includes several noteworthy features - in this article I'll cover the sanitizeProjection option, which addresses a new potential security issue in MongoDB 4.4.

Projections in MongoDB 4.4

One of the major internal achievements in MongoDB 4.4 was unifying queries and aggregations. As of MongoDB 4.4, the MongoDB server now handles find(filter) as a aggregate([{ $match: filter }]) under the hood. However, this has a somewhat surprising consequence: MongoDB 4.4 treats filter(filter).select(fields) as aggregate([{ $match: filter }, { $project: fields }]), including the $project stage's support for aliases.

In other words, if you rely on Mongoose's select to hide certain fields, a malicious user may be able to access hidden fields if you allow user-specified query projections. For example, suppose you use select to hide a password field. The below query will replace the value of the user's name property with the value of the password property, even though password is excluded by default.

const schema = new Schema({
  name: String,
  password: { type: String, select: false }
});
const UserModel = mongoose.model('User', schema);

const { _id } = await UserModel.create({
  name: 'my name',
  password: 'my secret'
});

const user = await UserModel.findById(_id).select({ name: '$password' });
user.name; // 'my secret' even though `password` is deselected!

For many apps, this isn't a problem. However, this new behavior may be a problem if your app allows user-specified projections. For example, the below Express endpoint is vulnerable because a malicious user can make an HTTP request with the query string ?name=$password to get the user's password hash.

app.get('/user/:id', function(req, res) {
  UserModel.findById(id).select(req.query.filter).then(
    doc => res.json(doc),
    err => res.status(500).json({ message: err.message })
  )
});

The sanitizeProjection Option

Mongoose 5.13 includes a sanitizeProjection option that lets you opt in to Mongoose replacing any strings in projections with the number 1. This means there's no way to access a non-selected field from a query projection, so you can use user-specified data for projections without any manual validation.

mongoose.set('sanitizeProjection', true);

const schema = new Schema({
  name: String,
  password: { type: String, select: false }
});
const UserModel = mongoose.model('User', schema);

const { _id } = await UserModel.create({
  name: 'my name',
  password: 'my secret'
});

const user = await UserModel.findById(_id).select({ name: '$password' });
user.name; // 'my name'

Like many other Mongoose options, you can configure sanitizeProjection globally, or on an individual query. sanitizeProjection is disabled by default, but you can enable it for a single query as shown below.

const user = await UserModel.findById(_id).select(projection).setOptions({
  sanitizeProjection: true
});

Moving On

MongoDB 4.4 made major changes in how MongoDB handles query projections. Before MongoDB 4.4, using user-specified projections in queries was safe because projections were highly limited. As of MongoDB 4.4, projections can pull in other fields, including ones that are explicitly excluded. The sanitizeProjection option can make your app more secure Custom casting is just one of 17 new features in Mongoose 5.13. Make sure you upgrade to take advantage of optimistic concurrency and all the other new features!

Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus