CVE-2022-4724 (Python)

Improper Access Control in GitHub repository ikus060/rdiffweb prior to 2.5.5.

Weakness:
CWE-284: name: Improper Access Control
description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.

Repository: https://github.com/ikus060/rdiffweb

Vulnerable code:
file_path: rdiffweb/core/model/__init__.py
snippet: @event.listens_for(Base.metadata, 'after_create')
def db_after_create(target, connection, **kw):
    """
    Called on database creation to update database schema.
    """

    if getattr(connection, '_transaction', None):
        connection._transaction.commit()

    # Add repo's Encoding
    _column_add(connection, RepoObject.__table__.c.Encoding)
    _column_add(connection, RepoObject.__table__.c.keepdays)

    # Create column for roles using "isadmin" column. Keep the
    # original column in case we need to revert to previous version.
    if not _column_exists(connection, UserObject.__table__.c.role):
        _column_add(connection, UserObject.__table__.c.role)
        UserObject.query.filter(UserObject._is_admin == 1).update({UserObject.role: UserObject.ADMIN_ROLE})

    # Add user's fullname column
    _column_add(connection, UserObject.__table__.c.fullname)

    # Add user's mfa column
    _column_add(connection, UserObject.__table__.c.mfa)

    # Re-create session table if Number column is missing
    if not _column_exists(connection, SessionObject.__table__.c.Number):
        SessionObject.__table__.drop()
        SessionObject.__table__.create()

    if getattr(connection, '_transaction', None):
        connection._transaction.commit()

    # Remove preceding and leading slash (/) generated by previous
    # versions. Also rename '.' to ''
    result = RepoObject.query.all()
    for row in result:
        if row.repopath.startswith('/') or row.repopath.endswith('/'):
            row.repopath = row.repopath.strip('/')
            row.commit()
        if row.repopath == '.':
            row.repopath = ''
            row.commit()
    # Remove duplicates and nested repositories.
    result = RepoObject.query.order_by(RepoObject.userid, RepoObject.repopath).all()
    prev_repo = (None, None)
    for row in result:
        if prev_repo[0] == row.userid and (prev_repo[1] == row.repopath or row.repopath.startswith(prev_repo[1] + '/')):
            row.delete()
        else:
            prev_repo = (row.userid, row.repopath)

    # Fix username case insensitive unique
    if not _index_exists(connection, 'user_username_index'):
        duplicate_users = (
            UserObject.query.with_entities(func.lower(UserObject.username))
            .group_by(func.lower(UserObject.username))
            .having(func.count(UserObject.username) > 1)
        ).all()
        try:
            user_username_index.create()
        except IntegrityError:
            msg = (
                'Failure to upgrade your database to make Username case insensitive. '
                'You must downgrade and deleted duplicate Username. '
                '%s' % '\n'.join([str(k) for k in duplicate_users]),
            )
            logger.error(msg)
            print(msg, file=sys.stderr)
            raise SystemExit(12)

file_path: rdiffweb/core/model/_user.py
snippet:     def add_authorizedkey(self, key, comment=None):
        """
        Add the given key to the user. Adding the key to his `authorized_keys`
        file if it exists and adding it to database.
        """
        # Parse and validate ssh key
        assert key
        key = authorizedkeys.check_publickey(key)

        # Remove option, replace comments.
        key = authorizedkeys.AuthorizedKey(
            options=None, keytype=key.keytype, key=key.key, comment=comment or key.comment
        )

        # If a filename exists, use it by default.
        filename = os.path.join(self.user_root, '.ssh', 'authorized_keys')
        if os.path.isfile(filename):
            with open(filename, mode="r+", encoding='utf-8') as fh:
                if authorizedkeys.exists(fh, key):
                    raise DuplicateSSHKeyError(_("SSH key already exists"))
                logger.info("add key [%s] to [%s] authorized_keys", key, self.username)
                authorizedkeys.add(fh, key)
        else:
            # Also look in database.
            logger.info("add key [%s] to [%s] database", key, self.username)
            try:
                SshKey(userid=self.userid, fingerprint=key.fingerprint, key=key.getvalue()).add().flush()
            except IntegrityError:
                raise DuplicateSSHKeyError(
                    _("Duplicate key. This key already exists or is associated to another user.")
                )
        cherrypy.engine.publish('user_attr_changed', self, {'authorizedkeys': True})
        cherrypy.engine.publish('authorizedkey_added', self, fingerprint=key.fingerprint, comment=comment)

Fixed code:
file_path: rdiffweb/core/model/__init__.py
snippet: def db_after_create(target, connection, **kw):
    """
    Called on database creation to update database schema.
    """

    if getattr(connection, '_transaction', None):
        connection._transaction.commit()

    # Add repo's Encoding
    _column_add(connection, RepoObject.__table__.c.Encoding)
    _column_add(connection, RepoObject.__table__.c.keepdays)

    # Create column for roles using "isadmin" column. Keep the
    # original column in case we need to revert to previous version.
    if not _column_exists(connection, UserObject.__table__.c.role):
        _column_add(connection, UserObject.__table__.c.role)
        UserObject.query.filter(UserObject._is_admin == 1).update({UserObject.role: UserObject.ADMIN_ROLE})

    # Add user's fullname column
    _column_add(connection, UserObject.__table__.c.fullname)

    # Add user's mfa column
    _column_add(connection, UserObject.__table__.c.mfa)

    # Re-create session table if Number column is missing
    if not _column_exists(connection, SessionObject.__table__.c.Number):
        SessionObject.__table__.drop()
        SessionObject.__table__.create()

    if getattr(connection, '_transaction', None):
        connection._transaction.commit()

    # Remove preceding and leading slash (/) generated by previous
    # versions. Also rename '.' to ''
    result = RepoObject.query.all()
    for row in result:
        if row.repopath.startswith('/') or row.repopath.endswith('/'):
            row.repopath = row.repopath.strip('/')
            row.commit()
        if row.repopath == '.':
            row.repopath = ''
            row.commit()
    # Remove duplicates and nested repositories.
    result = RepoObject.query.order_by(RepoObject.userid, RepoObject.repopath).all()
    prev_repo = (None, None)
    for row in result:
        if prev_repo[0] == row.userid and (prev_repo[1] == row.repopath or row.repopath.startswith(prev_repo[1] + '/')):
            row.delete()
        else:
            prev_repo = (row.userid, row.repopath)

    # Fix username case insensitive unique
    if not _index_exists(connection, 'user_username_index'):
        duplicate_users = (
            UserObject.query.with_entities(func.lower(UserObject.username))
            .group_by(func.lower(UserObject.username))
            .having(func.count(UserObject.username) > 1)
        ).all()
        try:
            user_username_index.create()
        except IntegrityError:
            msg = (
                'Failure to upgrade your database to make Username case insensitive. '
                'You must downgrade and deleted duplicate Username. '
                '%s' % '\n'.join([str(k) for k in duplicate_users]),
            )
            logger.error(msg)
            print(msg, file=sys.stderr)
            raise SystemExit(12)

    # Fix SSH Key uniqueness - since 2.5.4
    if not _index_exists(connection, 'sshkey_fingerprint_index'):
        duplicate_sshkeys = (
            SshKey.query.with_entities(SshKey.fingerprint)
            .group_by(SshKey.fingerprint)
            .having(func.count(SshKey.fingerprint) > 1)
        ).all()
        try:
            sshkey_fingerprint_index.create()
        except IntegrityError:
            msg = (
                'Failure to upgrade your database to make SSH Keys unique. '
                'You must downgrade and deleted duplicate SSH Keys. '
                '%s' % '\n'.join([str(k) for k in duplicate_sshkeys]),
            )
            logger.error(msg)
            print(msg, file=sys.stderr)
            raise SystemExit(12)

file_path: rdiffweb/core/model/_sshkey.py
snippet: class SshKey(Base):
    __tablename__ = 'sshkeys'
    __table_args__ = {'sqlite_autoincrement': True}
    fingerprint = Column('Fingerprint', Text)
    key = Column('Key', Text, unique=True, primary_key=True)
    userid = Column('UserID', Integer, nullable=False)


# Make finger print unique
sshkey_fingerprint_index = Index('sshkey_fingerprint_index', SshKey.fingerprint, unique=True)

file_path: rdiffweb/core/model/_user.py
snippet:     def add_authorizedkey(self, key, comment=None):
        """
        Add the given key to the user. Adding the key to his `authorized_keys`
        file if it exists and adding it to database.
        """
        # Parse and validate ssh key
        assert key
        key = authorizedkeys.check_publickey(key)

        # Remove option & Remove comment for SQL storage
        key = authorizedkeys.AuthorizedKey(
            options=None, keytype=key.keytype, key=key.key, comment=comment or key.comment
        )

        # If a filename exists, use it by default.
        filename = os.path.join(self.user_root, '.ssh', 'authorized_keys')
        if os.path.isfile(filename):
            with open(filename, mode="r+", encoding='utf-8') as fh:
                if authorizedkeys.exists(fh, key):
                    raise DuplicateSSHKeyError(_("SSH key already exists"))
                logger.info("add key [%s] to [%s] authorized_keys", key, self.username)
                authorizedkeys.add(fh, key)
        else:
            # Also look in database.
            logger.info("add key [%s] to [%s] database", key, self.username)
            try:
                sshkey = SshKey(userid=self.userid, fingerprint=key.fingerprint, key=key.getvalue())
                sshkey.add().flush()
            except IntegrityError:
                raise DuplicateSSHKeyError(
                    _("Duplicate key. This key already exists or is associated to another user.")
                )
        cherrypy.engine.publish('user_attr_changed', self, {'authorizedkeys': True})
        cherrypy.engine.publish('authorizedkey_added', self, fingerprint=key.fingerprint, comment=comment)