Skip to content

Commit 87677b8

Browse files
committed
Merge commit '422791ea45713aaaa865bdca74addb9fffd93a71'
2 parents 6b19e52 + 422791e commit 87677b8

File tree

5 files changed

+96
-39
lines changed

5 files changed

+96
-39
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ Breaking changes:
136136
* `session-dir` is deprecated and should be replace by `rate-limit-dir`. User's session are stored in database.
137137
* previous `.css` customization are not barkward compatible
138138

139+
## 2.4.5 (2002-09-16)
140+
141+
This releases include a security fix. If you are using an earlier version, you should upgrade to this release immediately.
142+
143+
* Mitigate CSRF on repository deletion and user deletion [CVE-2022-3232](https://nvd.nist.gov/vuln/detail/CVE-2022-3232) #214 #215
144+
139145
## 2.4.4 (2002-09-15)
140146

141147
This releases include a security fix. If you are using an earlier version, you should upgrade to this release immediately.
@@ -146,7 +152,7 @@ This releases include a security fix. If you are using an earlier version, you s
146152

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

149-
* Mitigate CSRF in profile's SSH Keys #212
155+
* Mitigate CSRF in profile's SSH Keys [CVE-2022-3221](https://nvd.nist.gov/vuln/detail/CVE-2022-3221) #212
150156

151157
## 2.4.2 (2022-09-12)
152158

rdiffweb/controller/page_admin_users.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ def default(self, username=None, action=u"", **kwargs):
233233
else:
234234
flash(_("Cannot edit user `%s`: user doesn't exists") % username, level='error')
235235
elif action == 'delete':
236-
self._delete_user(action, DeleteUserForm())
236+
form = DeleteUserForm()
237+
if form.validate_on_submit():
238+
self._delete_user(action, form)
237239

238240
params = {
239241
"add_form": UserForm(formdata=None),

rdiffweb/controller/page_delete.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,30 @@
2222
# Define the logger
2323

2424
import logging
25+
import os
2526

2627
import cherrypy
27-
from wtforms import validators
2828
from wtforms.fields.core import StringField
29+
from wtforms.validators import DataRequired, ValidationError
2930

3031
from rdiffweb.controller import Controller
3132
from rdiffweb.controller.dispatch import poppath
3233
from rdiffweb.controller.filter_authorization import is_maintainer
3334
from rdiffweb.controller.form import CherryForm
3435
from rdiffweb.core.librdiff import AccessDeniedError, DoesNotExistError
3536
from rdiffweb.core.model import RepoObject
37+
from rdiffweb.core.rdw_templating import url_for
3638
from rdiffweb.tools.i18n import gettext_lazy as _
3739

3840
_logger = logging.getLogger(__name__)
3941

4042

4143
class DeleteRepoForm(CherryForm):
42-
confirm = StringField(_('Confirmation'), validators=[validators.data_required()])
43-
redirect = StringField(default='/')
44+
confirm = StringField(_('Confirmation'), validators=[DataRequired()])
45+
46+
def validate_confirm(self, field):
47+
if self.confirm.data != self.expected_confirm:
48+
raise ValidationError(_('Invalid value, must be: %s') % self.expected_confirm)
4449

4550

4651
@poppath()
@@ -62,20 +67,18 @@ def default(self, path=b"", **kwargs):
6267

6368
# validate form
6469
form = DeleteRepoForm()
65-
if not form.validate():
66-
raise cherrypy.HTTPError(400, form.error_message)
67-
68-
# Validate the name
69-
if form.confirm.data != path_obj.display_name:
70-
_logger.info("do not delete repo, bad confirmation %r != %r", form.confirm.data, path_obj.display_name)
71-
raise cherrypy.HTTPError(400, 'bad confirmation')
72-
73-
# Delete repository in background using a schedule task.
74-
repo.expire()
75-
scheduled = cherrypy.engine.publish('schedule_task', self.delete_repo_path, repo.repoid, path)
76-
assert scheduled
77-
raise cherrypy.HTTPRedirect(form.redirect.data)
7870

79-
def delete_repo_path(self, repoid, path):
80-
repo = RepoObject.query.filter(RepoObject.repoid == repoid).first()
81-
repo.delete(path)
71+
form.expected_confirm = path_obj.display_name
72+
if form.is_submitted():
73+
if form.validate():
74+
cherrypy.engine.publish('schedule_task', repo.delete, path)
75+
# Redirect to parent folder or to root if repo get deleted
76+
if path_obj.isroot:
77+
raise cherrypy.HTTPRedirect(url_for('/'))
78+
else:
79+
parent_path = repo.fstat(os.path.dirname(path_obj.path))
80+
raise cherrypy.HTTPRedirect(url_for('browse', repo, parent_path))
81+
else:
82+
raise cherrypy.HTTPError(400, form.error_message)
83+
else:
84+
raise cherrypy.HTTPError(405)

rdiffweb/controller/tests/test_page_admin_users.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ def test_delete_user_admin(self):
278278
self.assertStatus(200)
279279
self.assertInBody("can't delete admin user")
280280

281+
def test_delete_user_method_get(self):
282+
# Given a user
283+
UserObject.add_user('newuser')
284+
# When trying to delete this user using method GET
285+
self.getPage("/admin/users/?action=delete&username=newuser", method='GET')
286+
# Then page return without error
287+
self.assertStatus(200)
288+
# Then user is not deleted
289+
self.assertIsNotNone(UserObject.get_user('newuser'))
290+
281291
def test_change_password_with_too_short(self):
282292
self._edit_user(self.USERNAME, password='short')
283293
self.assertInBody("Password must have between 8 and 128 characters.")

rdiffweb/controller/tests/test_page_delete.py

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,33 +35,59 @@ class DeleteRepoTest(rdiffweb.test.WebCase):
3535

3636
login = True
3737

38-
def _delete(self, user, repo, confirm, redirect=None):
38+
def _delete(self, user, repo, confirm):
3939
body = {}
4040
if confirm is not None:
4141
body.update({'confirm': confirm})
42-
if redirect is not None:
43-
body['redirect'] = redirect
4442
self.getPage("/delete/" + user + "/" + repo + "/", method="POST", body=body)
4543

4644
@parameterized.expand(
4745
[
48-
("with_dir", 'admin', '/testcases/Revisions', 'Revisions', 303, 404),
46+
("with_dir", 'admin', '/testcases/Revisions', 'Revisions', 303, 404, '/browse/admin/testcases'),
4947
("with_dir_wrong_confirmation", 'admin', '/testcases/Revisions', 'invalid', 400, 200),
50-
("with_file", 'admin', '/testcases/Revisions/Data', 'Data', 303, 404),
48+
("with_file", 'admin', '/testcases/Revisions/Data', 'Data', 303, 404, '/browse/admin/testcases/Revisions'),
5149
("with_file_wrong_confirmation", 'admin', '/testcases/Revisions/Data', 'invalid', 400, 200),
5250
("with_invalid", 'admin', '/testcases/invalid', 'invalid', 404, 404),
53-
("with_broken_symlink", 'admin', '/testcases/BrokenSymlink', 'BrokenSymlink', 303, 404),
54-
("with_utf8", 'admin', '/testcases/R%C3%A9pertoire%20Existant', 'Répertoire Existant', 303, 404),
51+
(
52+
"with_broken_symlink",
53+
'admin',
54+
'/testcases/BrokenSymlink',
55+
'BrokenSymlink',
56+
303,
57+
404,
58+
'/browse/admin/testcases',
59+
),
60+
(
61+
"with_utf8",
62+
'admin',
63+
'/testcases/R%C3%A9pertoire%20Existant',
64+
'Répertoire Existant',
65+
303,
66+
404,
67+
'/browse/admin/testcases',
68+
),
5569
("with_rdiff_backup_data", 'admin', '/testcases/rdiff-backup-data', 'rdiff-backup-data', 404, 404),
56-
("with_quoted_path", 'admin', '/testcases/Char%20%3B090%20to%20quote', 'Char Z to quote', 303, 404),
70+
(
71+
"with_quoted_path",
72+
'admin',
73+
'/testcases/Char%20%3B090%20to%20quote',
74+
'Char Z to quote',
75+
303,
76+
404,
77+
'/browse/admin/testcases',
78+
),
5779
]
5880
)
5981
@skipIf(rdiff_backup_version() < (2, 0, 1), "rdiff-backup-delete is available since 2.0.1")
60-
def test_delete_path(self, unused, username, path, confirmation, expected_status, expected_history_status):
82+
def test_delete_path(
83+
self, unused, username, path, confirmation, expected_status, expected_history_status, expected_redirect=None
84+
):
6185
# When trying to delete a file or a folder with a confirmation
6286
self._delete(username, path, confirmation)
6387
# Then a status is returned
6488
self.assertStatus(expected_status)
89+
if expected_redirect:
90+
self.assertHeaderItemValue('Location', self.baseurl + expected_redirect)
6591
# Check filesystem
6692
sleep(1)
6793
self.getPage("/history/" + username + "/" + path)
@@ -77,6 +103,7 @@ def test_delete_repo(self):
77103
# Delete repo
78104
self._delete(self.USERNAME, self.REPO, 'testcases')
79105
self.assertStatus(303)
106+
self.assertHeaderItemValue('Location', self.baseurl + '/')
80107
# Check filesystem
81108
sleep(1)
82109
userobj.expire()
@@ -90,6 +117,7 @@ def test_delete_repo_with_slash(self):
90117
# Then delete it.
91118
self._delete(self.USERNAME, self.REPO, 'testcases')
92119
self.assertStatus(303)
120+
self.assertHeaderItemValue('Location', self.baseurl + '/')
93121
# Check filesystem
94122
sleep(1)
95123
userobj.expire()
@@ -132,10 +160,9 @@ def test_delete_repo_as_admin(self):
132160
user_obj.refresh_repos()
133161
self.assertEqual(['broker-repo', 'testcases'], [r.name for r in user_obj.repo_objs])
134162

135-
self._delete('anotheruser', 'testcases', 'testcases', redirect='/admin/repos/')
163+
self._delete('anotheruser', 'testcases', 'testcases')
136164
self.assertStatus(303)
137-
location = self.assertHeader('Location')
138-
self.assertTrue(location.endswith('/admin/repos/'))
165+
self.assertHeaderItemValue('Location', self.baseurl + '/')
139166

140167
# Check filesystem
141168
sleep(1)
@@ -156,11 +183,10 @@ def test_delete_repo_as_maintainer(self):
156183
# Login as maintainer
157184
self._login('maintainer', 'password')
158185

159-
# Try to delete own own repo
160-
self._delete('maintainer', 'testcases', 'testcases', redirect='/admin/repos/')
186+
# Try to delete your own repo
187+
self._delete('maintainer', 'testcases', 'testcases')
161188
self.assertStatus(303)
162-
location = self.assertHeader('Location')
163-
self.assertTrue(location.endswith('/admin/repos/'))
189+
self.assertHeaderItemValue('Location', self.baseurl + '/')
164190

165191
# Check filesystem
166192
sleep(1)
@@ -179,8 +205,8 @@ def test_delete_repo_as_user(self):
179205
# Login as maintainer
180206
self._login('user', 'password')
181207

182-
# Try to delete our own repo
183-
self._delete('user', 'testcases', 'testcases', redirect='/admin/repos/')
208+
# Try to delete own own repo
209+
self._delete('user', 'testcases', 'testcases')
184210
self.assertStatus(403)
185211

186212
# Check database don't change
@@ -194,3 +220,13 @@ def test_delete_repo_does_not_exists(self):
194220
self._delete(self.USERNAME, repo, repo)
195221
# Then a 404 is return to the user
196222
self.assertStatus(404)
223+
224+
def test_delete_method_get(self):
225+
# Given a user with repo
226+
self.assertEqual(['broker-repo', 'testcases'], [r.name for r in UserObject.get_user('admin').repo_objs])
227+
# When trying to deleted repo with GET method
228+
self.getPage("/delete/" + self.USERNAME + "/" + self.REPO + "/?confirm=" + self.REPO, method="GET")
229+
# Then An error is returned
230+
self.assertStatus(405)
231+
# Then repo still exists
232+
self.assertEqual(['broker-repo', 'testcases'], [r.name for r in UserObject.get_user('admin').repo_objs])

0 commit comments

Comments
 (0)