NicholasFolk
NicholasFolk

Reputation: 1137

Does modelObject.save() only update an existing database document when the modelObject was obtained from the database itself?

To use an example that demonstrates the question, assume I have a User model defined by the following schema:

var UserSchema = new Schema({ 
    username: String,
    email: String
}
mongoose.model('User', UserSchema);

I know that to update a user using the save method, I could query the user and then save changes like so:

User.findOne({username: usernameTofind}, function(err, user) {
    //ignore errors for brevity
    user.email = newEmail;
    user.save(function(err) { console.log('User email updated') });
});

But if I try to create a new User object with the exact same field values (including the _id) is there any possibility of overwriting the database document? I would assume not, because in theory this would mean that a malicious user could exploit an insecure api and overwrite existing documents (for instance using a 'Create a New Account' request, which wouldn't/couldn't rely on the user already being authenticated) , but more importantly, when I try to do this using a request tool (I'm using Postman, but I'm sure a similar curl command would suffice), I get a duplicate _id error

MongoError: insertDocument :: caused by :: 11000 E11000 duplicate key error index

So I just want to clarify that the only way to update an existing document is to query for the document, modify the returned instance, then call the save method on that instance, OR use the static update(). Both of these could be secured by requiring authentication.

If it helps, my motivation for this question is mentioned above, in that I want to make sure a user is not able to overwrite an existing document if a method such as the following is exposed publicly:

userCtrl.create = function(req, res, next) {
    var user = new User(req.body);

    user.save(function(err) {
        if (err) {
            return next(err);
        } else {
            res.json(user);
        }
    });
};

Quick Edit: I just realized, if this is the case, then how does the database know the difference between the queried instance and a new User object with the exact same keys and properties?

Upvotes: 1

Views: 56

Answers (1)

victorkt
victorkt

Reputation: 14572

Does modelObject.save() only update an existing database document when the modelObject was obtained from the database itself?

Yes, it does. There is a flag that indicates if the document is new or not. If it is new, Mongoose will insert the document. If not, then it will update the document. The flag is Document#isNew.

When you find a document:

User.findOne({username: usernameTofind}, function(err, user) {
    //ignore errors for brevity
    console.log(user.isNew) // => will return false
});

When you create a new instance:

var user = new User(req.body);
console.log(user.isNew) // => will return true

So I just want to clarify that the only way to update an existing document is to query for the document, modify the returned instance, then call the save method on that instance, OR use the static update(). Both of these could be secured by requiring authentication.

There are other ways you can update documents, using Model#update, Model.findOneAndUpdate and others.

However, you can't update an _id field. MongoDB won't allow it even if Mongoose didn't already issue the proper database command. If you try it you will get something like this error:

The _id field cannot be changed from {_id: ObjectId('550d93cbaf1e9abd03bf0ad2')} to {_id: ObjectId('550d93cbaf1e9abd03bf0ad3')}.

But assuming you are using the last piece of code in your question to create new users, Mongoose will issue an insert command, so there is no way someone could overwrite an existing document. Even if it passes an _id field in the request body, MongoDB will throw a E11000 duplicate key error index error.

Also, you should filter the fields a user can pass as payload before you use them to create the user. For example, you could create a generic function that receives an object and an array of allowed parameters:

module.exports = function(object, allowedParams) {
    return Object.keys(object).reduce(function(newObject, param) {
        if (allowedParams.indexOf(param) !== -1)
            newObject[param] = object[param];

        return newObject;
    }, {});
}

And then you only require and use the function to filter the request body:

var allow = require('./parameter-filter');

function filter(params) {
    return allow(params, ["username", "email"]);
}

var user = new User(filter(req.body));

Upvotes: 1

Related Questions