Chapter 12: Methods

Methods

In the previous chapter, we talked about the first of two major security issues that come included with every Meteor project by default. That issue was the ability for users to navigate through all of the data inside the database.

Based on the changes we made, users now only have access to data that “belongs” to them.

To demonstrate the second major security issue, enter the following command into the Console:

PlayersList.insert();

Then, between the parentheses, pass through whatever data you want in the JSON format. You should end up with something like this:

PlayersList.insert({
    name: "Fake Player",
    score: 1000,
    unwantedData: "Hello!"
});

Can you see the problem?

We’ve made it so users can’t navigate through the project’s data, but users can still freely insert data into the database by using the Console. This means any user could fill the database with any type (and any amount) of unwanted data, which is a gaping hole in our application’s security. Users also have to ability to modify and remove data from the database, meaning that, by default, they basically have complete, administrative permissions.

This functionality is contained within a package named “insecure”, and while it’s convenient functionality during development, it’s something we’ll need to turn off before we share the application with the world.

To remove this package from the project, run the following command:

meteor remove insecure

Then, once the package has been removed, switch back to the browser and try playing around with the application. You’ll notice that:

  1. Users can no longer add players to the list.
  2. Users can no longer remove players from the list.
  3. Users can no longer give points to players.
  4. Users can no longer take points from players.

Basically, all of the insert, update, and remove functions have stopped working on the client-side. This includes from inside the Console. (If you try to use the functions in the Console, an error will be returned.)

As a result, the application is a lot more secure, but we’ll need to make some changes to get the interface working again, while still preventing users from manipulating the database through the Console.

Create a Method

Up until this point, the insert, update, and remove functions have been sitting inside our events.

This has been the easiest way to get the functions working as desired, but it’s also partly why our application has been insecure. We’ve been placing these sensitive, database-related functions on the client-side, inside the user’s web browser – a location that we don’t have much control over.

The secure approach is to move these functions into methods, and methods are simply blocks of code that can be triggered from elsewhere in an application.

To demonstrate how methods work, let’s create one.

Inside the JavaScript file, write the following code outside of the isClient and isServer conditionals:

Meteor.methods({
    // methods go here
});

Within this block, we can define numerous methods, similar to how we define both helpers and events.

To define our first method, start by choosing a name:

Meteor.methods({
    'createPlayer'
});

Here, we’re calling this method “createPlayer”.

Next, associate a function with this method:

Meteor.methods({
    'createPlayer': function(){
        // this method's function
    }
});

Then, inside this function, create a console.log statement:

Meteor.methods({
    'createPlayer': function(){
        console.log("Hello world");
    }
});

Return to the “submit form” event that’s inside the isClient conditional and place the following statement after the insert function:

Meteor.call('createPlayer');

The event should then resemble:

'submit form': function(event){
    event.preventDefault();
    var playerNameVar = event.target.playerName.value;
    var currentUserId = Meteor.userId();
    PlayersList.insert({
        name: playerNameVar,
        score: 0,
        createdBy: currentUserId
    });
    Meteor.call('createPlayer');
    event.target.playerName.value = "";
}

Here, we’re using this Meteor.call function to call a method, which simply means to trigger the execution of the method that we’ve passed through between the parentheses – in this case, we’re triggering the “createPlayer” method.

This means, whenever the user adds a player to the list by submitting the form, the code inside the “createPlayer” will be executed.

To demonstrate this, switch back to the browser and use the form.

The insert function still won’t work, meaning a player won’t be added to the list, but the “Hello world” message from the method will appear in the Console – thereby confirming that the method is working as it should.

To get the insert function working again, move both it and the “currentUserId” variable from the “submit form” event and into the “createPlayer” method:

Meteor.methods({
    'createPlayer': function(){
        var currentUserId = Meteor.userId();
        PlayersList.insert({
            name: playerNameVar,
            score: 0,
            createdBy: currentUserId
        });
    }
});

But replace the reference to “playerNameVar” with a static value, such as a string of “David”:

Meteor.methods({
    'createPlayer': function(){
        var currentUserId = Meteor.userId();
        PlayersList.insert({
            name: "David",
            score: 0,
            createdBy: currentUserId
        });
    }
});

This is because we’re not quite ready to pass dynamic values in the method, although that is something we’ll work on in a moment.

As for the “submit form” event, it should now resemble the following:

'submit form': function(event){
    event.preventDefault();
    var playerNameVar = event.target.playerName.value;
    Meteor.call('createPlayer');
    event.target.playerName.value = "";
}

Based on these changes, the “Add Player” form will now kind of work. If we submit the form, a player of “David” will be added to the list:

But if we try to use the insert function in the Console, we still won’t have the permission to do so.

This is because we’ve starting to gain control over how users interact with the database. After we removed the “insecure” package, we made it so users can only interact with the database by executing a method, and since we’re the ones defining how methods work, it’s much easier for us to control exactly what users are able to do.

Optimistic UI

In earlier versions of this book, I suggested that methods should be made to run on the server, from inside the isServer conditional.

But this is not actually the best approach.

We can place methods inside the isServer conditional and then call those methods from the isClient conditional, and in some cases this is necessary, but in the majority of cases, it’s not ideal.

As such, I just want to take a moment to explain why methods should generally be placed outside of these conditionals since it’s a very important part of what makes Meteor such an interesting framework.

Earlier, we talked about how code that is placed outside of the isClient and isServer conditionals will execute on both the client (from inside the user’s web browser) and on the server (where the application is hosted). We also talked about how a single statement (or a single chunk of code) can behave differently depending on the environment within which that code is run.

This is something that happens in the case of methods, and to explain how, we’ll focus on the “createPlayer” method that we’ve created.

When the “createPlayer” method is executed on the server, it behaves as you would expect it to behave: by grabbing the unique ID of the currently logged-in user and inserting that data into the “PlayersList” collection.

There’s no surprises there.

But when the “createPlayer” method is executed on the client, Meteor “guesses” what the method is trying to do on the server and instantly reflects those changes from inside the browser.

When we’re adding a player to the list, for instance, the change will happen inside the interface without any delay. The user will simply see that player’s name and score appear inside the list. But, behind the scenes, it actually takes a little bit longer for the data to be inserted into the database, since the data has to travel from the user’s web browser and to the server. Because of the way Meteor guesses what the method is trying to do though, users will never see this delay. As far as they’re concerned, everything happens in an instant, which is great from a user experience perspective, but there’s a lot more going on than they will ever realize.

This feature is part of what Meteor refers to as “Optimistic UI”, and the creators of Meteor have written a great article about it if you’re looking for further details. We’ve only covered a fragment of the feature, although since all of this happens transparently, even developers don’t really have to spend much time thinking about it. It just helps to have a general understanding so you know why you’re putting certain code in certain places.

Passing Arguments

For the “Add Player” form to work as expected, we need move the value of the “playerNameVar” variable from the “submit form” event and into the “createPlayer” method. This will allow users to create players with unique name fields, rather than every player on the list being named “David”.

To do this, pass the “playerNameVar” variable into the Meteor.call statement as a second argument:

'submit form': function(event){
    event.preventDefault();
    var playerNameVar = event.target.playerName.value;
    Meteor.call('createPlayer', playerNameVar);
    event.target.playerName.value = "";
}

Then allow the “createPlayer” method to accept this argument by referencing it between the parentheses of the method’s function:

Meteor.methods({
    'createPlayer': function(playerNameVar){
        var currentUserId = Meteor.userId();
        PlayersList.insert({
            name: "David",
            score: 0,
            createdBy: currentUserId
        });
    }
});

Based on this change, we can now reference “playerNameVar” from inside the method to reference the value that the user enters into the “playerName” text field.

This means, inside the method’s insert function, we can pass “playerNameVar” into the name field, rather than using the static value of “David”:

Meteor.methods({
    'createPlayer': function(playerNameVar){
        var currentUserId = Meteor.userId();
        PlayersList.insert({
            name: playerNameVar,
            score: 0,
            createdBy: currentUserId
        });
    }
});

As a result, the “Add Player” form will work as expected, but users still won’t be able to use the insert function from inside the Console.

But something that might not be completely obvious is the fact that users can call methods from inside the Console.

For example, any user can execute the “createPlayer” method, and because the method accepts an argument, they can attach any value to the method.

This means all of the following methods could be called by any user:

Meteor.call('createPlayer', 'Unwanted Data');
Meteor.call('createPlayer', false);
Meteor.call('createPlayer', 42);

But, in each case, the user doesn’t have to be logged-in to run these statements, meaning the data that is ultimately added to the database, and in the latter two statements, the data type – a boolean value and a number – is inappropriate for the type of data that should be used for a player’s name.

This means, although we’ve gained some control over how users interact with the database by creating our own methods, the method we’ve created so far can still be exploited.

Of course, a user does have to figure out there’s a method named “createPlayer” before they’re able to start trying to exploit it, but that’s not particularly difficult to do. Even after we deploy the application, users can still view the source of the web application and view the JavaScript code:

These methods are running on the client, after all, so all of that code will continue to face the public, even after we’ve secured everything else.

As such, there are two problems we need to account for:

First, we need to ensure that only users can execute these methods. So even if the user chooses to run them in the Console, for whatever reason, all of the data will at least be attached to their account (rather than floating around in the database, unassigned to any user in particular).

Second, we need to make sure users can only pass a string into the method. In other words, we need to check that the argument for the “createPlayer” method is of a predefined object type. This ensures that users can’t break the application by passing anything other than some text into the method.

We’ll solve both of these problems in the next section.

Validating Data

Another package that the Meteor Development Group includes with every project by default is known as the “check” package, and the “check” package allows us to write check functions that, appropriately enough, check whether a certain piece of data is of a certain type or not.

Basically, we can use the “check” package to ask questions like, “Is this variable a string?”

To install this package, run the following command:

meteor add check

Then place a check function at the top of the “createPlayer” method:

check();

This function accepts two arguments.

The first argument will be the piece of data that we want to check is of a certain type. In our case, we want to check that the “playerNameVar” variable is a String, so we’ll pass through “playerNameVar” as this argument:

check(playerNameVar);

The second argument will be the object type that we’re expecting. Since we’re expecting a string, we’ll pass through an argument of String:

check(playerNameVar, String);

Because of this code, if a user tries to execute the method by passing through a value that is not a string, an error will be returned and the rest of the method won’t be executed. You can test these with the following statements:

Meteor.call('createPlayer', false);
Meteor.call('createPlayer', 42);

Neither of these methods will successfully execute because neither of the values being passed into them are strings.

The second problem is that, currently, logged-out users are able to call the “createPlayer” method from inside the Console. This means any user could fill the database with meaningless data that isn’t attached to any user’s account.

To fix this, return to the “createPlayer” create a conditional that surrounds the insert function:

Meteor.methods({
    'createPlayer': function(playerNameVar){
        check(playerNameVar, String);
        var currentUserId = Meteor.userId();
        if(currentUserId){
            PlayersList.insert({
                name: playerNameVar,
                score: 0,
                createdBy: currentUserId
            });
        }
    }
});

Here, we’re checking to see if the “currentUserId” variable returns true. This allows us to determine if the current user is logged-in because, if the current user is not logged-in, then the Meteor.userId function – and therefore, the “currentUserId” variable – will return false.

Based on this change, users must be logged-in to use the “createPlayer” method, which means the method is now considerably more secure.

Removing Players (Again)

The next step is to create a method for the “Remove Player” button. As a result, users will be able to remove players from the list, but they still won’t be able to use the remove function from inside the Console.

To begin, delete the remove function from the “click .remove” event:

'click .remove': function(){
    var selectedPlayer = Session.get('selectedPlayer');
}

…and replace it with a Meteor.call statement:

'click .remove': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    Meteor.call('removePlayer');
}

Here, we’re calling this “removePlayer” method, which is a method that we’ll setup in a moment.

As the second argument, pass through the “selectedPlayer” variable. This will allow us to access the ID of the selected player from inside the method, which is necessary for removing them from the database:

'click .remove': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    Meteor.call('removePlayer', selectedPlayer);
}

Inside the Meteor.methods block, create the “removePlayer” method:

'removePlayer': function(){
    // the code for the method
}

(As is the case with helpers and events, we can create multiple methods in a single block of code by separating them with commas.)

Make it so the method can accept the “selectedPlayer” argument:

'removePlayer': function(selectedPlayer){
    // the code for the method
}

Then recreate the remove function from inside the method:

'removePlayer': function(selectedPlayer){
    PlayersList.remove({ _id: selectedPlayer });
}

Because of this code, the button will work as expected, but users still won’t be able to use the remove function from within the Console. Once again, the use of methods allow us to control how users interact with the database.

There are, however, a couple of things for us to consider:

First, we have to check to see if the “selectedPlayer” argument is a string, which can be achieved with the check function:

'removePlayer': function(selectedPlayer){
    check(selectedPlayer, String);
    PlayersList.remove({ _id: selectedPlayer });
}

Second, we need to prevent logged-out users from being able to execute this method, which can be done by creating a “currentUserId” variable:

'removePlayer': function(selectedPlayer){
    check(selectedPlayer, String);
    var currentUserId = Meteor.userId();
    PlayersList.remove({ _id: selectedPlayer });
}

Then by using the same conditional that we covered before:

'removePlayer': function(selectedPlayer){
    check(selectedPlayer, String);
    var currentUserId = Meteor.userId();
    if(currentUserId){
        PlayersList.remove({ _id: selectedPlayer });
    }
}

Third, we have to account for the fact that, based on this code, any logged-in user could execute the “removePlayer” method from the Console and pass through the ID of a player that doesn’t belong to them. This means a user could delete players from another user’s leaderboard.

Is this likely to happen?

No.

A user would have to first discover the ID of a player’s document, and it’s an awful lot of effort for relatively little gain. Even so, it’s your responsibility to ensure the safety of your user’s data – no matter how “small” and “unlikely” potential interference and intrusion might seem.

To solve this problem, change the query inside the remove function so it will only remove a document from the collection if that document belongs to the currently logged-in user:

'removePlayer': function(selectedPlayer){
    check(selectedPlayer, String);
    var currentUserId = Meteor.userId();
    if(currentUserId){
        PlayersList.remove({ _id: selectedPlayer, createdBy: currentUserId });
    }
}

This slight addition makes it so, even if a user is logged-in, executing the “removePlayer” method will only remove a player from the database if the ID of a player is specified and that player is attached to the logged-in user.

As such, we now have multiple layers of security protecting the user’s data.

Modifying Scores

Methods are useful for the sake of security, but they’re also a convenient way to reduce the amount of code we have to maintain.

To demonstrate this, we’re going to merge the “click .increment” and “click .decrement” events into a single method, reducing the repetition of our code while also securing things along the way.

As you might recall, this is what the “click .increment” and “click .decrement” events look like at the moment:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer }, {$inc: {score: 5} });
},
'click .decrement': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer }, {$inc: {score: -5} });
}

The only notable difference between these events is that, inside the “click .increment” event, we’re passing a value of 5 into the $inc operator, while inside the “decrement” event, we’re passing through a value of -5.

To remedy this redundancy, we’ll start by focusing on the “click .increment” event.

Inside this event, remove the update function and replace it with a Meteor.call statement:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    Meteor.call();
}

Pass through a value of “updateScore” as the first argument:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    Meteor.call('updateScore');
}

Then pass through the “selectedPlayer” variable as the second argument, so the method (which we’ll create in a moment) will have access to the ID of the currently selected player:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    Meteor.call('updateScore', selectedPlayer);
}

Next, create the “updateScore” method inside the Meteor.methods block:

'updateScore': function(){
    // the code goes here
}

Allow this method to accepted the “selectedPlayer” argument:

'updateScore': function(selectedPlayer){
    // the code goes here
}

Then recreate the update function inside the method:

'updateScore': function(selectedPlayer){
    PlayersList.update( { _id: selectedPlayer },
                        { $inc: {score: 5} });
}

Based on this code, the “Give 5 Points” button will work as expected.

But to make the method more useful, pass a value of 5 into the Meteor.call statement as a third argument:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    Meteor.call('updateScore', selectedPlayer, 5);
}

…and, from inside the method, refer to this argument as “scoreValue”:

'updateScore': function(selectedPlayer, scoreValue){
    PlayersList.update( { _id: selectedPlayer },
                        { $inc: {score: scoreValue} });
}

Because of this change, the method is now flexible enough that we can use it for both the “Give 5 Points” button and the “Take 5 Points” button. All we have to do is add the Meteor.call statement to the “decrement” event, but instead of passing through a value of 5 as the third argument, we can pass through a value of -5:

'click .decrement': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    Meteor.call('updateScore', selectedPlayer, -5);
}

Both buttons will work as expected, but with less code involved.

There are, however, a few problems inside the “updateScore” method that we still need to solve.

First, we need to use the check function to ensure that the “selectedPlayer” argument is a string:

'updateScore': function(selectedPlayer, scoreValue){
    check(selectedPlayer, String);
    PlayersList.update( { _id: selectedPlayer },
                        { $inc: {score: scoreValue} });
}

Second, we need to use the check function to ensure that the “scoreValue” argument is a number:

'updateScore': function(selectedPlayer, scoreValue){
    check(selectedPlayer, String);
    check(scoreValue, Number);
    PlayersList.update( { _id: selectedPlayer },
                        { $inc: {score: scoreValue} });
}

Third, we need to setup another “currentUserId” variable:

'updateScore': function(selectedPlayer, scoreValue){
    check(selectedPlayer, String);
    check(scoreValue, Number);
    var currentUserId = Meteor.userId();
    PlayersList.update( { _id: selectedPlayer },
                        { $inc: {score: scoreValue} });
}

…and create a conditional around the update function to prevent users from executing this method if they’re not logged into an account:

'updateScore': function(selectedPlayer, scoreValue){
    check(selectedPlayer, String);
    check(scoreValue, Number);
    var currentUserId = Meteor.userId();
    if(currentUserId){
        PlayersList.update( { _id: selectedPlayer },
                            { $inc: {score: scoreValue} });
    }
}

Fourth, pass the “currentUserId” variable into the update function, so logged-in users can only update documents that belong to them:

'updateScore': function(selectedPlayer, scoreValue){
    check(selectedPlayer, String);
    check(scoreValue, Number);
    var currentUserId = Meteor.userId();
    if(currentUserId){
        PlayersList.update( { _id: selectedPlayer, createdBy: currentUserId },
                            { $inc: {score: scoreValue} });
    }
}

Despite these changes though, this code still isn’t perfect. If a user wanted to, they could:

  • Remove points from a player until that player has less than zero points.
  • Call the “updatePlayer” method from the Console and pass through an empty string in place of a name.
  • Call the “updatePlayer” method from the Console and pass through whatever number they want for the score (such as an absurdly high number).

These problems aren’t particularly urgent, nor do they require any wizardry specific to Meteor, but if you’re inclined to make this project work as well as it possibly can, you might like to take some time to fix them.

Summary

In this chapter, we’ve learned that:

  • By default, all users of a Meteor application can insert, update, and remove data from a Meteor project’s collections using the JavaScript Console. This is convenient during development, but also a security hole that must be fixed.
  • This functionality is contained within a package known as “insecure”. By removing this package, the application will become a lot more secure, but it will also break. We’ll need to make some changes to get it working again.
  • To create methods, we define a methods block, and then trigger them from elsewhere in the code using Meteor.call statements.
  • We can pass data into the Meteor.call statements, allowing us to use the submitted data from a form within the method.
  • Users can execute the Meteor.call statements from inside the Console, so we need to be mindful of exactly what those statements allow them to do.
  • Before we allow any interaction with a database, it’s important that we validate data by using the check function.

To gain a deeper understanding of Meteor:

  • In a new project, start by removing the “insecure” package. Then create the Leaderboard application from scratch, using methods from the very beginning.

To browse the project’s code in its current state, click here.