meteormeteor-methods

Making Vote System Secure Meteor.Methods


I am trying to implement a voting system which is secure and cant be changed with a Meteor.call() from the client console. Everyone is able to vote some posts up and down if they are logged in. But just once a time for every user and post.

Ony my client side ive got something like this:

Template.postArgument.events({
 'click .yes':function() {
       if(Meteor.user()) {
        var postId = Arguments.findOne({_id:this._id})
        var currentArgumentId = this._id;
        if($.inArray(Meteor.userId(), postId.votedUp) ===-1) {
          if($.inArray(Meteor.userId(), postId.votedDown) !==-1) {
            Meteor.call('argumentVoteYes',currentArgumentId);
          } else {
            Meteor.call('argumentVoteYesElse',currentArgumentId);
          } 
        } else {
          Meteor.call('argumentVoteYesElseElse',currentArgumentId);
        }
      }
    }}
)};

On my Server:

    Meteor.methods({
    'argumentVoteYes':function(currentArgumentId){
        Arguments.update(currentArgumentId, {
            $pull: {votedDown: Meteor.userId()},
            $inc: {score: 2 },
                $addToSet: {votedUp: Meteor.userId() }
              });
      },
      'argumentVoteYesElse':function(currentArgumentId){
        Arguments.update(currentArgumentId, {
            $inc: {score: 1 },
            $addToSet: {votedUp: Meteor.userId() }
              });
      },
      'argumentVoteYesElseElse':function(currentArgumentId){
        Arguments.update(currentArgumentId, {
            $inc: {score: -1 },
            $pull: {votedUp: Meteor.userId()}
            });
      }
    'argumentVoteNo':function(currentArgumentId){
    Arguments.update(currentArgumentId, {
        $pull: {votedUp: Meteor.userId()},
        $inc: {score: -2 },
        $addToSet: {votedDown: Meteor.userId() },
        });
  },
  'argumentVoteNoElse':function(currentArgumentId){
    Arguments.update(currentArgumentId, {
        $inc: {score: -1 },
        $addToSet: {votedDown: Meteor.userId() },
        });

  },
  'argumentVoteNoElseElse':function(currentArgumentId){
    Arguments.update(currentArgumentId, {
        $inc: {score: 1 },
        $pull: {votedDown: Meteor.userId()}
        }); 
  },
    });

The question is how do i get this secure for example if someone calls a Meteor.call('argumentvoteYes', "someID" , {$inc: {score:2}}); It will increment the score of 2. If a user is calling this twice the vote will increment 4. Is there any way doing this a secure way?


Solution

  • You don't have to worry about an extra increment coming in to the method since your methods only accept a single parameter. However you do need to guard against other hacks:

    Let's first extend the Match object so that we can check that an _id is just that:

    Match._id = Match.Where(function(id){
      check(id, String); // first make sure we're dealing with a string
      // then grep for an exactly 17 character alphanumeric string
      return /^[a-zA-Z0-9]{17,17}/.test(id); 
    });
    

    more on this technique

    Now let's take your first method:

    Meteor.methods({
      'argumentVoteYes':function(currentArgumentId){
        check(currentArgumentId,Match._id); // will error if not an _id
    
        var post = Arguments.findOne({ _id: currentArgumentId });
        var userId = Meteor.userId(); // the current user
    
        if ( post && userId ){ // make sure a real user is operating on an actual document
          // only update if no votes have been recorded OR
          // user hasn't already voted
    
          if ( !post.votedUp || post.votedUp.indexOf(userId) === -1 ){ 
            Arguments.update(currentArgumentId, {
              $pull: { votedDown: userId },
              $inc: { score: 2 },
              $addToSet: {votedUp: userId }
            });
          }
        }
      },