CVE-2022-3232 (Python, published 2022-09-17T20:15Z)
Description: value: Cross-Site Request Forgery (CSRF) in GitHub repository ikus060/rdiffweb prior to 2.4.5.
Severity: nan (CVSS v3 base score: 4.3)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
Repository: https://github.com/ikus060/rdiffweb
Files changed:
README.md
rdiffweb/controller/page_admin.py
rdiffweb/controller/page_delete.py
rdiffweb/controller/pref_general.py
rdiffweb/controller/tests/test_page_admin.py
rdiffweb/controller/tests/test_page_delete.py
Commit message: Mitigate CSRF on repository deletion and user deletion #214 #215


Vulnerable code:
* Mitigate CSRF in profile's SSH Keys #212
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.password.validators += [
            validators.length(
                min=self.app.cfg.password_min_length,
                max=self.app.cfg.password_max_length,
                message=_('Password must have between %(min)d and %(max)d characters.'),
            )
        ]
            self._delete_user(action, DeleteUserForm())
from wtforms import validators
    confirm = StringField(_('Confirmation'), validators=[validators.data_required()])
    redirect = StringField(default='/')
        if not form.validate():
            raise cherrypy.HTTPError(400, form.error_message)

        # Validate the name
        if form.confirm.data != path_obj.display_name:
            _logger.info("do not delete repo, bad confirmation %r != %r", form.confirm.data, path_obj.display_name)
            raise cherrypy.HTTPError(400, 'bad confirmation')

        # Delete repository in background using a schedule task.
        scheduled = cherrypy.engine.publish('schedule_task', repo.delete, path)
        assert scheduled
        raise cherrypy.HTTPRedirect(form.redirect.data)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.new.validators += [
            Length(
                min=self.app.cfg.password_min_length,
                max=self.app.cfg.password_max_length,
                message=_('Password must have between %(min)d and %(max)d characters.'),
            )
        ]
    def _delete(self, user, repo, confirm, redirect=None):
        if redirect is not None:
            body['redirect'] = redirect
            ("with_dir", 'admin', '/testcases/Revisions', 'Revisions', 303, 404),
            ("with_file", 'admin', '/testcases/Revisions/Data', 'Data', 303, 404),
            ("with_broken_symlink", 'admin', '/testcases/BrokenSymlink', 'BrokenSymlink', 303, 404),
            ("with_utf8", 'admin', '/testcases/R%C3%A9pertoire%20Existant', 'Répertoire Existant', 303, 404),
            ("with_quoted_path", 'admin', '/testcases/Char%20%3B090%20to%20quote', 'Char Z to quote', 303, 404),
    def test_delete_path(self, unused, username, path, confirmation, expected_status, expected_history_status):
        self._delete('anotheruser', 'testcases', 'testcases', redirect='/admin/repos/')
        location = self.assertHeader('Location')
        self.assertTrue(location.endswith('/admin/repos/'))
        # Try to delete own own repo
        self._delete('maintainer', 'testcases', 'testcases', redirect='/admin/repos/')
        location = self.assertHeader('Location')
        self.assertTrue(location.endswith('/admin/repos/'))
        self._delete('user', 'testcases', 'testcases', redirect='/admin/repos/')

Fixed code:
## 2.4.5 (2002-09-16)

This releases include a security fix. If you are using an earlier version, you should upgrade to this release immediately.

* Mitigate CSRF on repository deletion and user deletion [CVE-2022-3232](https://nvd.nist.gov/vuln/detail/CVE-2022-3232) #214 #215

* Mitigate CSRF in profile's SSH Keys [CVE-2022-3221](https://nvd.nist.gov/vuln/detail/CVE-2022-3221) #212
    def validate_password(self, field):
        validator = validators.length(
            min=self.app.cfg.password_min_length,
            max=self.app.cfg.password_max_length,
            message=_('Password must have between %(min)d and %(max)d characters.'),
        )
        validator(self, field)
            form = DeleteUserForm()
            if form.validate_on_submit():
                self._delete_user(action, form)
import os
from wtforms.validators import DataRequired, ValidationError
from rdiffweb.core.rdw_templating import url_for
    confirm = StringField(_('Confirmation'), validators=[DataRequired()])

    def validate_confirm(self, field):
        if self.confirm.data != self.expected_confirm:
            raise ValidationError(_('Invalid value, must be: %s') % self.expected_confirm)
        form.expected_confirm = path_obj.display_name
        if form.is_submitted():
            if form.validate():
                cherrypy.engine.publish('schedule_task', repo.delete, path)
                # Redirect to parent folder or to root if repo get deleted
                if path_obj.isroot:
                    raise cherrypy.HTTPRedirect(url_for('/'))
                else:
                    parent_path = repo.fstat(os.path.dirname(path_obj.path))
                    raise cherrypy.HTTPRedirect(url_for('browse', repo, parent_path))
            else:
                raise cherrypy.HTTPError(400, form.error_message)
        else:
            raise cherrypy.HTTPError(405)
    def validate_new(self, field):
        validator = Length(
            min=self.app.cfg.password_min_length,
            max=self.app.cfg.password_max_length,
            message=_('Password must have between %(min)d and %(max)d characters.'),
        )
        validator(self, field)
    def test_delete_user_method_get(self):
        # Given a user
        self.app.store.add_user('newuser')
        # When trying to delete this user using method GET
        self.getPage("/admin/users/?action=delete&username=newuser", method='GET')
        # Then page return without error
        self.assertStatus(200)
        # Then user is not deleted
        self.assertIsNotNone(self.app.store.get_user('newuser'))

    def _delete(self, user, repo, confirm):
            ("with_dir", 'admin', '/testcases/Revisions', 'Revisions', 303, 404, '/browse/admin/testcases'),
            ("with_file", 'admin', '/testcases/Revisions/Data', 'Data', 303, 404, '/browse/admin/testcases/Revisions'),
            (
                "with_broken_symlink",
                'admin',
                '/testcases/BrokenSymlink',
                'BrokenSymlink',
                303,
                404,
                '/browse/admin/testcases',
            ),
            (
                "with_utf8",
                'admin',
                '/testcases/R%C3%A9pertoire%20Existant',
                'Répertoire Existant',
                303,
                404,
                '/browse/admin/testcases',
            ),
            (
                "with_quoted_path",
                'admin',
                '/testcases/Char%20%3B090%20to%20quote',
                'Char Z to quote',
                303,
                404,
                '/browse/admin/testcases',
            ),
    def test_delete_path(
        self, unused, username, path, confirmation, expected_status, expected_history_status, expected_redirect=None
    ):
        if expected_redirect:
            self.assertHeaderItemValue('Location', self.baseurl + expected_redirect)
        self.assertHeaderItemValue('Location', self.baseurl + '/')
        self.assertHeaderItemValue('Location', self.baseurl + '/')
        self._delete('anotheruser', 'testcases', 'testcases')
        self.assertHeaderItemValue('Location', self.baseurl + '/')
        # Try to delete your own repo
        self._delete('maintainer', 'testcases', 'testcases')
        self.assertHeaderItemValue('Location', self.baseurl + '/')
        self._delete('user', 'testcases', 'testcases')

    def test_delete_method_get(self):
        # Given a user with repo
        self.assertEqual(['broker-repo', 'testcases'], [r.name for r in self.app.store.get_user('admin').repo_objs])
        # When trying to deleted repo with GET method
        self.getPage("/delete/" + self.USERNAME + "/" + self.REPO + "/?confirm=" + self.REPO, method="GET")
        # Then An error is returned
        self.assertStatus(405)
        # Then repo still exists
        self.assertEqual(['broker-repo', 'testcases'], [r.name for r in self.app.store.get_user('admin').repo_objs])