CVE-2020-28953: Insufficient Information (NVD-CWE-noinfo)
value: In BigBlueButton before 2.2.29, a user can vote more than once in a single poll.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 4.3
Published: 2020-11-19T22:15Z
Repository: https://github.com/bigbluebutton/bigbluebutton
Language: JavaScript
Files changed: akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala, bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js, bigbluebutton-html5/imports/ui/components/polling/service.js
Commit date: 2020-10-28 18:33:09 +0000
Commit message: prevent users from being able to send more than 1 poll vote

Diff: diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala
index 4aab4ac88c3..2d34a5e4010 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala
@@ -311,11 +311,14 @@ object Polls {
   }
 
   def respondToQuestion(pollId: String, questionID: Int, responseID: Int, responder: Responder, polls: Polls) {
     polls.polls.get(pollId) match {
       case Some(p) => {
-        p.respondToQuestion(questionID, responseID, responder)
+        if (!p._responders.exists(_ == responder)) {
+          p.addResponder(responder)
+          p.respondToQuestion(questionID, responseID, responder)
+        }
       }
       case None =>
     }
   }
 
@@ -453,10 +456,15 @@ case class QuestionOutVO(id: String, multiResponse: Boolean, question: String, r
 class Poll(val id: String, val questions: Array[Question], val numRespondents: Int, val title: Option[String]) {
   private var _started: Boolean = false
   private var _stopped: Boolean = false
   private var _showResult: Boolean = false
   private var _numResponders: Int = 0
+  var _responders = new ArrayBuffer[Responder]()
+
+  def addResponder(responder: Responder) {
+    _responders += (responder)
+  }
 
   def showingResult() { _showResult = true }
   def showResult(): Boolean = { _showResult }
   def start() { _started = true }
   def stop() { _stopped = true }
diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js
index 6d5281e43e7..a7eba4a3a61 100644
--- a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js
+++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js
@@ -6,16 +6,25 @@ import { extractCredentials } from '/imports/api/common/server/helpers';
 
 export default function publishVote(pollId, pollAnswerId) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
   const EVENT_NAME = 'RespondToPollReqMsg';
-
   const { meetingId, requesterUserId } = extractCredentials(this.userId);
 
   check(pollAnswerId, Number);
   check(pollId, String);
 
+  const waitingFor = Polls.findOne({ id: pollId }, {
+    feilds: {
+      users: 1,
+    },
+  });
+
+  const userResponded = !waitingFor.users.includes(requesterUserId);
+
+  if (userResponded) return null;
+
   const selector = {
     users: requesterUserId,
     meetingId,
     'answers.id': pollAnswerId,
   };
@@ -41,13 +50,13 @@ export default function publishVote(pollId, pollAnswerId) {
   const cb = (err) => {
     if (err) {
       return Logger.error(`Removing responded user from Polls collection: ${err}`);
     }
 
-    return Logger.info(`Removed responded user=${requesterUserId} from poll (meetingId: ${meetingId}, `
+    Logger.info(`Removed responded user=${requesterUserId} from poll (meetingId: ${meetingId}, `
       + `pollId: ${pollId}!)`);
+
+    return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
   };
 
   Polls.update(selector, modifier, cb);
-
-  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
 }
diff --git a/bigbluebutton-html5/imports/ui/components/polling/service.js b/bigbluebutton-html5/imports/ui/components/polling/service.js
index 40c241ec099..e5ecaa8b812 100644
--- a/bigbluebutton-html5/imports/ui/components/polling/service.js
+++ b/bigbluebutton-html5/imports/ui/components/polling/service.js
@@ -1,10 +1,15 @@
 import { makeCall } from '/imports/ui/services/api';
 import Polls from '/imports/api/polls';
+import { debounce } from 'lodash';
 
 const MAX_CHAR_LENGTH = 5;
 
+const handleVote = (pollId, answerId) => {
+  makeCall('publishVote', pollId, answerId.id);
+};
+
 const mapPolls = () => {
   const poll = Polls.findOne({});
   if (!poll) {
     return { pollExists: false };
   }
@@ -28,12 +33,10 @@ const mapPolls = () => {
       pollId: poll.id,
       stackOptions,
     },
     pollExists: true,
     amIRequester,
-    handleVote(pollId, answerId) {
-      makeCall('publishVote', pollId, answerId.id);
-    },
+    handleVote: debounce(handleVote, 500, { leading: true, trailing: false }),
   };
 };
 
 export default { mapPolls };

Vulnerable code:         p.respondToQuestion(questionID, responseID, responder)

    return Logger.info(`Removed responded user=${requesterUserId} from poll (meetingId: ${meetingId}, `

  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
    handleVote(pollId, answerId) {
      makeCall('publishVote', pollId, answerId.id);
    },
Fixed code:         if (!p._responders.exists(_ == responder)) {
          p.addResponder(responder)
          p.respondToQuestion(questionID, responseID, responder)
        }
  var _responders = new ArrayBuffer[Responder]()

  def addResponder(responder: Responder) {
    _responders += (responder)
  }
  const waitingFor = Polls.findOne({ id: pollId }, {
    feilds: {
      users: 1,
    },
  });

  const userResponded = !waitingFor.users.includes(requesterUserId);

  if (userResponded) return null;

    Logger.info(`Removed responded user=${requesterUserId} from poll (meetingId: ${meetingId}, `

    return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
import { debounce } from 'lodash';
const handleVote = (pollId, answerId) => {
  makeCall('publishVote', pollId, answerId.id);
};

    handleVote: debounce(handleVote, 500, { leading: true, trailing: false }),