CVE-2020-27602 (JavaScript, published 2022-09-29T03:15Z)
Description: value: BigBlueButton before 2.2.7 does not have a protection mechanism for separator injection in meetingId, userId, and authToken.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
Repository: https://github.com/bigbluebutton/bigbluebutton
Files changed:
bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js
bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js
bigbluebutton-html5/imports/api/users/server/store/pendingAuthentications.js
Commit message: Refactor connection definition of userId to wait for validateAuthToken


Vulnerable code:
  const { userId, valid, waitForApproval } = body;
import Users from '/imports/api/users';
import createDummyUser from '../modifiers/createDummyUser';
import setConnectionIdAndAuthToken from '../modifiers/setConnectionIdAndAuthToken';
  const sessionId = `${meetingId}--${requesterUserId}`;
  this.setUserId(sessionId);

  const User = Users.findOne({
    meetingId,
    userId: requesterUserId,
  });

  if (!User) {
    createDummyUser(meetingId, requesterUserId, requesterToken);
  }

  setConnectionIdAndAuthToken(meetingId, requesterUserId, this.connection.id, requesterToken);
  Logger.info(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}'`);

Fixed code:
import pendingAuthenticationsStore from '../store/pendingAuthentications';
import createDummyUser from '../modifiers/createDummyUser';
import setConnectionIdAndAuthToken from '../modifiers/setConnectionIdAndAuthToken';
  const { userId, valid, authToken, waitForApproval } = body;
  check(authToken, String);
  const pendingAuths = pendingAuthenticationsStore.take(meetingId, userId, authToken);

  if(!valid) {
    pendingAuths.forEach ( 
      pendingAuth => {
        try {
          const {methodInvocationObject} = pendingAuth;
          const connectionId = methodInvocationObject.connection.id;

          methodInvocationObject.connection.close();
          Logger.info(`Closed connection ${connectionId} due to invalid auth token.`);
        } catch (e) {
          Logger.error(`Error closing socket for meetingId '${meetingId}', userId '${userId}', authToken ${authToken}`);
        }
      }
    );
    
    return;
  }

  if(valid) {
    // Define user ID on connections
    pendingAuths.forEach ( 
        pendingAuth => {
            const {methodInvocationObject} = pendingAuth;

            /* Logic migrated from validateAuthToken method ( postponed to only run in case of success response ) - Begin */
            const sessionId = `${meetingId}--${userId}`;
            methodInvocationObject.setUserId(sessionId);

            const User = Users.findOne({
                meetingId,
                userId: userId,
            });
        
            if (!User) {
                createDummyUser(meetingId, userId, authToken);
            }
        
            setConnectionIdAndAuthToken(meetingId, userId, methodInvocationObject.connection.id, authToken);
            /* End of logic migrated from validateAuthToken */
        }
    );
  }

import pendingAuthenticationsStore from '../store/pendingAuthentications';
  // Store reference of methodInvocationObject ( to postpone the connection userId definition )
  pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this);
  Logger.info(`User '${requesterUserId}' is trying to validate auth token for meeting '${meetingId}' from connection '${this.connection.id}'`);
import Logger from '/imports/startup/server/logger';

class PendingAuthentitcations {
    constructor () {
        Logger.debug("PendingAuthentitcations :: constructor");
        this.store = [];
    }

    generateKey (meetingId, userId, authToken) {
        // Protect against separator injection
        meetingId = meetingId.replace(/ /g, '');
        userId    = userId.replace(/ /g, '');
        authToken = authToken.replace(/ /g, '');

        // Space separated key
        return '${meetingId} ${userId} ${authToken}';
    }

    add (meetingId, userId, authToken, methodInvocationObject) {
        Logger.debug("PendingAuthentitcations :: add", {meetingId, userId, authToken});
        this.store.push({
            key: this.generateKey(meetingId, userId, authToken),
            meetingId, userId, authToken, methodInvocationObject
        });
    }

    take (meetingId, userId, authToken) {
        Logger.debug("PendingAuthentitcations :: take", {meetingId, userId, authToken});
        const key = this.generateKey(meetingId, userId, authToken);
        
        // find matches
        const matches = this.store.filter( e => e.key === key );
        
        // remove matches (if any)
        if(matches.length) { 
            this.store = this.store.filter( e => e.key !== key ) ;
        }

        // return matches
        return matches;
    }
}
export default new PendingAuthentitcations();