VYPR
Moderate severityNVD Advisory· Published Oct 10, 2022· Updated Aug 3, 2024

Open Redirect in ikus060/rdiffweb

CVE-2022-3438

Description

Open Redirect in GitHub repository ikus060/rdiffweb prior to 2.5.0a4.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An open redirect vulnerability in rdiffweb prior to 2.5.0a4 allows an attacker to redirect users to arbitrary external websites.

Vulnerability

Description

CVE-2022-3438 identifies an open redirect vulnerability in the rdiffweb application, a web interface for managing rdiff-backup repositories. The issue exists in versions prior to 2.5.0a4 and stems from insufficient validation of redirect parameters, allowing an attacker to craft a URL that redirects a user to an arbitrary external site. This type of vulnerability often arises when user-supplied input controls redirect targets without proper checks [1].

Exploitation

An attacker can exploit this by tricking a victim into clicking a crafted link that appears to lead to the legitimate rdiffweb instance. No authentication or advanced network position is required; the attack is performed on the client side. The commit fixing the issue (4d464b467f14b8eb9103d7f5f0774e49995527c7) shows that validation was enforced on fields like fullname, username, and email, suggesting the open redirect could stem from user profile inputs or login redirect functionality [3].

Impact

Successful exploitation allows the attacker to redirect a user to a malicious website, which could be used for phishing, credential theft, or malware distribution. Although this does not directly compromise the rdiffweb server or data, it undermines trust and can be used as a stepping stone for further attacks. The vulnerability has a CVSS score of 6.1 (Medium) as per the NVD listing [2], reflecting the medium severity due to the need for user interaction.

Mitigation

The vulnerability is patched in rdiffweb version 2.5.0a4. Users should upgrade to this version or later. The PyPI advisory database (PYSEC-2022-43158) also references this issue, confirming the patch availability [4]. No workarounds other than upgrading have been publicly documented.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
rdiffwebPyPI
< 2.5.0a42.5.0a4

Affected products

2
  • ghsa-coords
    Range: < 2.5.0a4
  • ikus060/ikus060/rdiffwebv5
    Range: unspecified

Patches

1
4d464b467f14

Enforce validation on fullname, username and email for increase security #224

https://github.com/ikus060/rdiffwebPatrik DufresneSep 30, 2022via ghsa
6 files changed · +106 75
  • rdiffweb/controller/page_admin_users.py+4 11 modified
    @@ -74,20 +74,24 @@ class UserForm(CherryForm):
             validators=[
                 validators.data_required(),
                 validators.length(max=256, message=_('Username too long.')),
    +            validators.length(min=3, message=_('Username too short.')),
    +            validators.regexp(UserObject.PATTERN_USERNAME, message=_('Must not contain any special characters.')),
             ],
         )
         fullname = StringField(
             _('Fullname'),
             validators=[
                 validators.optional(),
                 validators.length(max=256, message=_('Fullname too long.')),
    +            validators.regexp(UserObject.PATTERN_FULLNAME, message=_('Must not contain any special characters.')),
             ],
         )
         email = EmailField(
             _('Email'),
             validators=[
                 validators.optional(),
                 validators.length(max=256, message=_('Email too long.')),
    +            validators.regexp(UserObject.PATTERN_EMAIL, message=_('Must be a valid email address.')),
             ],
         )
         password = PasswordField(
    @@ -140,17 +144,6 @@ class UserForm(CherryForm):
             widget=widgets.HiddenInput(),
         )
     
    -    def __init__(self, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    -        cfg = cherrypy.tree.apps[''].cfg
    -        self.password.validators += [
    -            validators.length(
    -                min=cfg.password_min_length,
    -                max=cfg.password_max_length,
    -                message=_('Password must have between %(min)d and %(max)d characters.'),
    -            )
    -        ]
    -
         def validate_role(self, field):
             # Don't allow the user to changes it's "role" state.
             currentuser = cherrypy.request.currentuser
    
  • rdiffweb/controller/page_pref_general.py+4 10 modified
    @@ -19,23 +19,16 @@
     to change password ans refresh it's repository view.
     """
     
    -import logging
    -import re
    -
     import cherrypy
     from wtforms.fields import HiddenField, PasswordField, StringField, SubmitField
     from wtforms.fields.html5 import EmailField
     from wtforms.validators import DataRequired, EqualTo, InputRequired, Length, Optional, Regexp
     
     from rdiffweb.controller import Controller, flash
     from rdiffweb.controller.form import CherryForm
    +from rdiffweb.core.model import UserObject
     from rdiffweb.tools.i18n import gettext_lazy as _
     
    -# Define the logger
    -_logger = logging.getLogger(__name__)
    -
    -PATTERN_EMAIL = re.compile(r'[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$')
    -
     
     class UserProfileForm(CherryForm):
         action = HiddenField(default='set_profile_info')
    @@ -45,14 +38,15 @@ class UserProfileForm(CherryForm):
             validators=[
                 Optional(),
                 Length(max=256, message=_('Fullname too long.')),
    +            Regexp(UserObject.PATTERN_FULLNAME, message=_('Must not contain any special characters.')),
             ],
         )
         email = EmailField(
             _('Email'),
             validators=[
                 DataRequired(),
    -            Length(max=256, message=_("Invalid email.")),
    -            Regexp(PATTERN_EMAIL, message=_("Invalid email.")),
    +            Length(max=256, message=_("Email too long.")),
    +            Regexp(UserObject.PATTERN_EMAIL, message=_("Must be a valid email address.")),
             ],
         )
         set_profile_info = SubmitField(_('Save changes'))
    
  • rdiffweb/controller/tests/test_page_admin_users.py+46 27 modified
    @@ -18,6 +18,7 @@
     from unittest.mock import ANY, MagicMock
     
     import cherrypy
    +from parameterized import parameterized
     
     import rdiffweb.test
     from rdiffweb.core.model import UserObject
    @@ -170,40 +171,58 @@ def test_add_edit_delete(self):
             self.assertInBody("User account removed.")
             self.assertNotInBody("test2")
     
    -    def test_edit_fullname(self):
    +    @parameterized.expand(
    +        [
    +            # Invalid
    +            ('evil.com', False),
    +            ('http://test', False),
    +            ('email@test.test', False),
    +            ('/test/', False),
    +            # Valid
    +            ('My fullname', True),
    +            ('Test Test', True),
    +            ('Éric Terrien-Pascal', True),
    +            ("Tel'c", True),
    +        ]
    +    )
    +    def test_edit_fullname_with_special_character(self, new_fullname, expected_valid):
             # Given an existing user
             # When updating the user's fullname
             self.getPage(
                 "/admin/users/",
                 method='POST',
    -            body={'action': 'edit', 'username': self.USERNAME, 'fullname': 'My fullname'},
    +            body={'action': 'edit', 'username': self.USERNAME, 'fullname': new_fullname},
             )
             self.assertStatus(200)
    -        # Then user is updated successfully
    -        self.assertInBody("User information modified successfully.")
    -        # Then database is updated
    -        obj = UserObject.query.filter(UserObject.username == self.USERNAME).first()
    -        self.assertEqual('My fullname', obj.fullname)
    -
    -    def test_add_edit_delete_user_with_encoding(self):
    -        """
    -        Check creation of user with non-ascii char.
    -        """
    -        self._add_user("Éric", "éric@test.com", "pr3j5Dwi", "/home/", UserObject.USER_ROLE)
    -        self.assertInBody("User added successfully.")
    -        self.assertInBody("Éric")
    -        self.assertInBody("éric@test.com")
    -        # Update user
    -        self._edit_user("Éric", "eric.létourno@test.com", "écureuil", "/tmp/", UserObject.ADMIN_ROLE)
    -        self.assertInBody("User information modified successfully.")
    -        self.assertInBody("Éric")
    -        self.assertInBody("eric.létourno@test.com")
    -        self.assertNotInBody("/home/")
    -        self.assertInBody("/tmp/")
    -
    -        self._delete_user("Éric")
    -        self.assertInBody("User account removed.")
    -        self.assertNotInBody("Éric")
    +        if expected_valid:
    +            self.assertInBody("User information modified successfully.")
    +            self.assertNotInBody("Fullname: Must not contain any special characters.")
    +        else:
    +            self.assertNotInBody("User information modified successfully.")
    +            self.assertInBody("Fullname: Must not contain any special characters.")
    +
    +    @parameterized.expand(
    +        [
    +            # Invalid
    +            ('http://username', False),
    +            ('username@test.test', False),
    +            ('/username/', False),
    +            # Valid
    +            ('username.com', True),
    +            ('admin_user', True),
    +            ('test.test', True),
    +            ('test-test', True),
    +        ]
    +    )
    +    def test_add_user_with_special_character(self, new_username, expected_valid):
    +        self._add_user(new_username, "eric@test.com", "pr3j5Dwi", "/home/", UserObject.USER_ROLE)
    +        self.assertStatus(200)
    +        if expected_valid:
    +            self.assertInBody("User added successfully.")
    +            self.assertNotInBody("Username: Must not contain any special characters.")
    +        else:
    +            self.assertNotInBody("User added successfully.")
    +            self.assertInBody("Username: Must not contain any special characters.")
     
         def test_add_user_with_empty_username(self):
             """
    
  • rdiffweb/controller/tests/test_page_prefs_general.py+44 27 modified
    @@ -23,6 +23,7 @@
     from unittest.mock import MagicMock
     
     import cherrypy
    +from parameterized import parameterized
     
     import rdiffweb.test
     from rdiffweb.core.model import RepoObject, UserObject
    @@ -88,16 +89,31 @@ def test_change_username_noop(self):
             self.assertIsNotNone(user)
             self.assertEqual("test@test.com", user.email)
     
    -    def test_change_fullname(self):
    +    @parameterized.expand(
    +        [
    +            # Invalid
    +            ('@test.com', False),
    +            ('test.com', False),
    +            ('test@te_st.com', False),
    +            ('test@test.com, test2@test.com', False),
    +            # Valid
    +            ('test', True),
    +            ('My Fullname', True),
    +        ]
    +    )
    +    def test_change_fullname(self, new_fullname, expected_valid):
             # Given an authenticated user
             # When update the fullname
    -        self._set_profile_info("test@test.com", "My Fullname")
    +        self._set_profile_info("test@test.com", new_fullname)
             self.assertStatus(200)
    -        self.assertInBody("Profile updated successfully.")
    -        # Then database is updated with fullname
    -        self.assertInBody("My Fullname")
    -        user = UserObject.query.filter(UserObject.username == self.USERNAME).first()
    -        self.assertEqual("My Fullname", user.fullname)
    +        if expected_valid:
    +            self.assertInBody("Profile updated successfully.")
    +            # Then database is updated with fullname
    +            self.assertInBody(new_fullname)
    +            user = UserObject.query.filter(UserObject.username == self.USERNAME).first()
    +            self.assertEqual(new_fullname, user.fullname)
    +        else:
    +            self.assertNotInBody("Profile updated successfully.")
     
         def test_change_fullname_method_get(self):
             # Given an authenticated user
    @@ -126,30 +142,31 @@ def test_change_email(self):
             self.assertStatus(200)
             self.assertInBody("Profile updated successfully.")
     
    -    def test_change_email_with_invalid_email(self):
    -        self._set_profile_info("@test.com")
    -        self.assertStatus(200)
    -        self.assertInBody("Invalid email")
    -
    -        self._set_profile_info("test.com")
    -        self.assertStatus(200)
    -        self.assertInBody("Invalid email")
    -
    -        self._set_profile_info("test")
    -        self.assertStatus(200)
    -        self.assertInBody("Invalid email")
    -
    -        self._set_profile_info("test@te_st.com")
    -        self.assertStatus(200)
    -        self.assertInBody("Invalid email")
    -
    -        self._set_profile_info("test@test.com, test2@test.com")
    +    @parameterized.expand(
    +        [
    +            # Invalid
    +            ('@test.com', False),
    +            ('test.com', False),
    +            ('test', False),
    +            ('test@te_st.com', False),
    +            ('test@test.com, test2@test.com', False),
    +            # Valid
    +            ('test@test.com', True),
    +        ]
    +    )
    +    def test_change_email_with_invalid_email(self, new_email, expected_valid):
    +        self._set_profile_info(new_email)
             self.assertStatus(200)
    -        self.assertInBody("Invalid email")
    +        if expected_valid:
    +            self.assertInBody("Profile updated successfully.")
    +            self.assertNotInBody("Must be a valid email address.")
    +        else:
    +            self.assertNotInBody("Profile updated successfully.")
    +            self.assertInBody("Must be a valid email address.")
     
         def test_change_email_with_too_long(self):
             self._set_profile_info(("test1" * 50) + "@test.com")
    -        self.assertInBody("Invalid email")
    +        self.assertInBody("Email too long.")
     
         def test_change_password(self):
             self.listener.user_password_changed.reset_mock()
    
  • rdiffweb/core/model/_user.py+7 0 modified
    @@ -55,6 +55,7 @@ class UserObject(Base):
         __tablename__ = 'users'
         __table_args__ = {'sqlite_autoincrement': True}
     
    +    # Value for role.
         ADMIN_ROLE = 0
         MAINTAINER_ROLE = 5
         USER_ROLE = 10
    @@ -63,9 +64,15 @@ class UserObject(Base):
             'maintainer': MAINTAINER_ROLE,
             'user': USER_ROLE,
         }
    +    # Value for mfa field
         DISABLED_MFA = 0
         ENABLED_MFA = 1
     
    +    # Regex pattern to be used for validation.
    +    PATTERN_EMAIL = r"[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$"
    +    PATTERN_FULLNAME = r"""[^!"#$%&()*+,./:;<=>?@[\]_{|}~]+$"""
    +    PATTERN_USERNAME = r"[a-zA-Z0-9_.\-]+$"
    +
         userid = Column('UserID', Integer, primary_key=True)
         _username = Column('Username', String, nullable=False, unique=True)
         hash_password = Column('Password', String, nullable=False, default="")
    
  • README.md+1 0 modified
    @@ -131,6 +131,7 @@ This next release focus on two-factor-authentication as a measure to increase se
     * Add two-factor authentication with email verification #201
     * Generate a new session on login and 2FA #220
     * Enforce permission on /etc/rdiffweb configuration folder
    +* Enforce validation on fullname, username and email
     
     Breaking changes:
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.