decisionMaker
decisionMaker

Reputation: 101

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?

Upvotes: 0

Views: 73

Answers (3)

Luna
Luna

Reputation: 1178

I thought the same thing before and I found the solution in just using an array of userId's which you don't pass from client side (you just save current user's ID) and count total IDs in the array. It doesn't add the same ID twice.

From my code (you can do the rest of the checks like if the user is in disliker array, do something etc.):

likeActivity: function (activityId) {
    Activities.update(activityId,{
        $addToSet: {
            likers: this.userId,
        }
    });
},

unlikeActivity: function (activityId) {
    Activities.update(activityId,{
        $pull: {
            likers: this.userId,
        }
    })
}

In my helper:

likeCount: function() {
    return this.likers.length
}

Upvotes: 0

Michel Floyd
Michel Floyd

Reputation: 20256

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 }
        });
      }
    }
  },

Upvotes: 1

Stephen Woods
Stephen Woods

Reputation: 4049

You need to check for whether the user is in the votedUp/down arrays on the server. You have the logic right client side, so just apply the same logic before you do the actual update on the server.

Upvotes: 1

Related Questions