CVE-2022-3363 (Python, published 2022-10-26T21:15Z)
Description: value: Business Logic Errors in GitHub repository ikus060/rdiffweb prior to 2.5.0a7.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: NVD-CWE-Other \u2014 Other
NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
Repository: https://github.com/ikus060/rdiffweb
Files changed:
README.md
rdiffweb/controller/page_mfa.py
rdiffweb/controller/page_pref_mfa.py
rdiffweb/controller/tests/test_page_prefs_mfa.py
rdiffweb/core/config.py
rdiffweb/core/model/_user.py
rdiffweb/core/model/tests/test_user.py
rdiffweb/core/notification.py
rdiffweb/core/tests/test_notification.py
rdiffweb/templates/email_mfa.html
rdiffweb/templates/email_verification_code.html
tox.ini
Commit message: Send email notification when enabling or disabling MFA


Vulnerable code:
        else:
            code = cherrypy.tools.auth_mfa.generate_code()
            body = self.app.templates.compile_template(
                "email_mfa.html", **{"header_name": self.app.cfg.header_name, 'user': userobj, 'code': code}
            )
            cherrypy.engine.publish('queue_mail', to=userobj.email, subject=_("Your verification code"), message=body)
            flash(_("A new verification code has been sent to your email."))
            "email_mfa.html", **{"header_name": self.app.cfg.header_name, 'user': userobj, 'code': code}
from unittest.mock import MagicMock
        self.listener = MagicMock()
        cherrypy.engine.subscribe('queue_mail', self.listener.queue_email, priority=50)
        try:
            self.getPage("/mfa/")
            self.assertStatus(200)
            self.assertInBody("A new verification code has been sent to your email.")
            # Extract code from email between <strong> and </strong>
            self.listener.queue_email.assert_called_once()
            message = self.listener.queue_email.call_args[1]['message']
            code = message.split('<strong>', 1)[1].split('</strong>')[0]
            # Login to MFA
            self.getPage("/mfa/", method='POST', body={'code': code, 'submit': '1'})
            self.assertStatus(303)
        finally:
            cherrypy.engine.unsubscribe('queue_mail', self.listener.queue_email)
        # Register an email listeer to capture email send
        self.listener = MagicMock()
        cherrypy.engine.subscribe('queue_mail', self.listener.queue_email, priority=50)
        try:
            self.getPage("/prefs/mfa", method='POST', body={action: '1'})
            self.assertStatus(200)
            self.assertInBody("A new verification code has been sent to your email.")
            # Extract code from email between <strong> and </strong>
            self.listener.queue_email.assert_called_once()
            message = self.listener.queue_email.call_args[1]['message']
            return message.split('<strong>', 1)[1].split('</strong>')[0]
        finally:
            cherrypy.engine.unsubscribe('queue_mail', self.listener.queue_email)
            ('enable_mfa', UserObject.DISABLED_MFA, UserObject.ENABLED_MFA),
            ('disable_mfa', UserObject.ENABLED_MFA, UserObject.DISABLED_MFA),
    def test_with_valid_code(self, action, initial_mfa, expected_mfa):
        # Then no email get sent
        default=False,
from sqlalchemy.orm import deferred, relationship
    _username = Column('Username', String, nullable=False, unique=True)
    _user_root = Column('UserRoot', String, nullable=False, default="")
    _email = Column('UserEmail', String, nullable=False, default="")
    _role = Column('role', SmallInteger, nullable=False, server_default=str(USER_ROLE))
    def add_user(cls, username, password=None, **attrs):
    @hybrid_property
    def username(self):
        return self._username

    @username.setter
    def username(self, value):
        oldvalue = self._username
        self._username = value
        if oldvalue != value:
            cherrypy.engine.publish('user_attr_changed', self, {'username': (oldvalue, value)})

    @hybrid_property
    def role(self):
        if self._role is None:
            return self.USER_ROLE
        return self._role

    @role.setter
    def role(self, value):
        oldvalue = self._role
        self._role = value
        if oldvalue != value:
            cherrypy.engine.publish('user_attr_changed', self, {'role': (oldvalue, value)})

    @hybrid_property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        oldvalue = self._email
        self._email = value
        if oldvalue != value:
            cherrypy.engine.publish('user_attr_changed', self, {'email': (oldvalue, value)})

    @hybrid_property
    def user_root(self):
        return self._user_root

    @user_root.setter
    def user_root(self, value):
        oldvalue = self._user_root
        self._user_root = value
        if oldvalue != value:
            cherrypy.engine.publish('user_attr_changed', self, {'user_root': (oldvalue, value)})

    default_config = {
        'email-send-changed-notification': True,
    }

        if 'email' not in attrs:
            return

        old_email = attrs['email'][0]
        if not old_email:
            logger.info("can't sent mail to user [%s] without an email", userobj.username)
            return

        # If the email attributes was changed, send a mail notification.
        body = self.app.templates.compile_template(
            "email_changed.html", **{"header_name": self.app.cfg.header_name, 'user': userobj}
        )
        self.bus.publish('queue_mail', to=old_email, subject=_("Email address changed"), message=body)
      {% trans %}To help us make sure it's really you, here's the verification code you'll need to log in:{% endtrans %}
      <strong>{{ code }}</strong>
    </p>
    <p>
      {% trans %}If this wasn't you logging in, and you use a password to log in, please reset your password.{% endtrans %}
    </p>
    <p>
      {% trans %}This code will expire in 1 hour. Once the code expires, you will need to request a new verification code by going through the login procedure again.{% endtrans %}
deps = djlint==1.12.1

Fixed code:
* Send email notification when enabling or disabling MFA [CVE-2022-3363](https://nvd.nist.gov/vuln/detail/CVE-2022-3363)
            return
        code = cherrypy.tools.auth_mfa.generate_code()
        body = self.app.templates.compile_template(
            "email_verification_code.html", **{"header_name": self.app.cfg.header_name, 'user': userobj, 'code': code}
        )
        cherrypy.engine.publish('queue_mail', to=userobj.email, subject=_("Your verification code"), message=body)
        flash(_("A new verification code has been sent to your email."))
            "email_verification_code.html", **{"header_name": self.app.cfg.header_name, 'user': userobj, 'code': code}
from unittest.mock import ANY, MagicMock
        # Register a listener on email
        self.listener = MagicMock()
        cherrypy.engine.subscribe('queue_mail', self.listener.queue_email, priority=50)

    def tearDown(self):
        cherrypy.engine.unsubscribe('queue_mail', self.listener.queue_email)
        return super().tearDown()
        # Reset mock.
        self.listener.queue_email.reset_mock()
        # Leave to disable mfa
        self.getPage("/mfa/")
        self.assertStatus(200)
        self.assertInBody("A new verification code has been sent to your email.")
        # Extract code from email between <strong> and </strong>
        self.listener.queue_email.assert_called_once()
        message = self.listener.queue_email.call_args[1]['message']
        code = message.split('<strong>', 1)[1].split('</strong>')[0]
        # Login to MFA
        self.getPage("/mfa/", method='POST', body={'code': code, 'submit': '1'})
        self.assertStatus(303)
        # Clear mock.
        self.listener.queue_email.reset_mock()
        self.getPage("/prefs/mfa", method='POST', body={action: '1'})
        self.assertStatus(200)
        self.assertInBody("A new verification code has been sent to your email.")
        # Extract code from email between <strong> and </strong>
        self.listener.queue_email.assert_called_once()
        message = self.listener.queue_email.call_args[1]['message']
        self.listener.queue_email.reset_mock()
        return message.split('<strong>', 1)[1].split('</strong>')[0]
            ('enable_mfa', UserObject.DISABLED_MFA, UserObject.ENABLED_MFA, "Two-Factor Authentication turned on"),
            ('disable_mfa', UserObject.ENABLED_MFA, UserObject.DISABLED_MFA, "Two-Factor Authentication turned off"),
    def test_with_valid_code(self, action, initial_mfa, expected_mfa, expected_subject):
        # Then no verification code get sent
        # Then an email confirmation get send
        self.listener.queue_email.assert_called_once_with(to=ANY, subject=expected_subject, message=ANY)
        default=True,
from sqlalchemy.orm import deferred, relationship, validates
    username = Column('Username', String, nullable=False, unique=True)
    user_root = Column('UserRoot', String, nullable=False, default="")
    email = Column('UserEmail', String, nullable=False, default="")
    role = Column('role', SmallInteger, nullable=False, server_default=str(USER_ROLE), default=USER_ROLE)
    def add_user(cls, username, password=None, role=USER_ROLE, **attrs):
            role=role,
    @validates('username')
    def validates_username(self, key, value):
        if self.username:
            raise ValueError('Username cannot be modified.')
        return value


@event.listens_for(UserObject, 'after_update')
def user_attr_changed(mapper, connection, target):
    changes = {}
    state = inspect(target)
    for attr in state.attrs:
        if attr.key in ['user_root', 'email', 'role', 'mfa']:
            hist = attr.load_history()
            if hist.has_changes():
                changes[attr.key] = (
                    hist.deleted[0] if len(hist.deleted) >= 1 else None,
                    hist.added[0] if len(hist.added) >= 1 else None,
                )
    if changes:
        cherrypy.engine.publish('user_attr_changed', target, changes)
        user = UserObject.get_user('larry')
        user.add()
        user = UserObject.get_user('larry')
        user.add()
        if 'email' in attrs:
            old_email = attrs['email'][0]
            if not old_email:
                logger.info("can't sent mail to user [%s] without an email", userobj.username)
                return
            # If the email attributes was changed, send a mail notification.
            subject = _("Email address changed")
            body = self.app.templates.compile_template(
                "email_changed.html", **{"header_name": self.app.cfg.header_name, 'user': userobj}
            )
            self.bus.publish('queue_mail', to=old_email, subject=str(subject), message=body)

        if 'mfa' in attrs:
            if not userobj.email:
                logger.info("can't sent mail to user [%s] without an email", userobj.username)
                return
            subject = (
                _("Two-Factor Authentication turned off")
                if userobj.mfa == UserObject.DISABLED_MFA
                else _("Two-Factor Authentication turned on")
            )
            body = self.app.templates.compile_template(
                "email_mfa.html", **{"header_name": self.app.cfg.header_name, 'user': userobj}
            )
            self.bus.publish('queue_mail', to=userobj.email, subject=str(subject), message=body)
        user.add()
        user = UserObject.get_user(self.USERNAME)
        user.add()
      {% if user.mfa %}
        {% trans %}Your {{ header_name }} Account is now protected with Two-Factor Authentication. When you sign in on a new or untrusted device, you'll need your second factor to verify your identity.{% endtrans %}
      {% else %}
        {% trans %}Your {{ header_name }} account is no longer protected with Two-Factor Authentication. You don't need your second factor to sign in.{% endtrans %}
      {% endif %}
      {% trans %}You received this email to let you know about important changes to your Google Account and services.{% endtrans %}
<html>
  <head></head>
  <body>
    {% trans username=(user.fullname or user.username) %}Hey {{ username }},{% endtrans %}
    <p>
      {% trans %}To help us make sure it's really you, here's the verification code you'll need to log in:{% endtrans %}
    </p>
    <p>
      <strong>{{ code }}</strong>
    </p>
    <p>
      {% trans %}If this wasn't you logging in, and you use a password to log in, please reset your password.{% endtrans %}
    </p>
    <p>
      {% trans %}This code will expire in 1 hour. Once the code expires, you will need to request a new verification code by going through the login procedure again.{% endtrans %}
    </p>
  </body>
</html>
deps = djlint==1.19.2