From 65ca78e9e7577c60108e5d6f8e8435ce5e541f9e Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:13:58 -0800 Subject: [PATCH] Security: fix/GHSA-7qqc-wrcw-2fj9 --- src/paperless_mail/tests/test_mail.py | 29 +++++++++++++++++++++++++++ src/paperless_mail/views.py | 20 +++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index a0eed1635..c239c9f33 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -1815,6 +1815,35 @@ class TestMailAccountTestView(APITestCase): expected_str = "Unable to refresh oauth token" self.assertIn(expected_str, error_str) + def test_mail_account_test_view_existing_forbidden_for_other_owner(self): + other_user = User.objects.create_user( + username="otheruser", + password="testpassword", + ) + existing_account = MailAccount.objects.create( + name="Owned account", + imap_server="imap.example.com", + imap_port=993, + imap_security=MailAccount.ImapSecurity.SSL, + username="admin", + password="secret", + owner=other_user, + ) + data = { + "id": existing_account.id, + "imap_server": "imap.example.com", + "imap_port": 993, + "imap_security": MailAccount.ImapSecurity.SSL, + "username": "admin", + "password": "****", + "is_token": False, + } + + response = self.client.post(self.url, data, format="json") + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content.decode(), "Insufficient permissions") + class TestMailAccountProcess(APITestCase): def setUp(self): diff --git a/src/paperless_mail/views.py b/src/paperless_mail/views.py index b54bcb5f7..b3b7f812e 100644 --- a/src/paperless_mail/views.py +++ b/src/paperless_mail/views.py @@ -86,13 +86,26 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin): request.data["name"] = datetime.datetime.now().isoformat() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + existing_account = None + account_id = request.data.get("id") # account exists, use the password from there instead of *** and refresh_token / expiration if ( len(serializer.validated_data.get("password").replace("*", "")) == 0 - and request.data["id"] is not None + and account_id is not None ): - existing_account = MailAccount.objects.get(pk=request.data["id"]) + try: + existing_account = MailAccount.objects.get(pk=account_id) + except (TypeError, ValueError, MailAccount.DoesNotExist): + return HttpResponseBadRequest("Invalid account") + + if not has_perms_owner_aware( + request.user, + "change_mailaccount", + existing_account, + ): + return HttpResponseForbidden("Insufficient permissions") + serializer.validated_data["password"] = existing_account.password serializer.validated_data["account_type"] = existing_account.account_type serializer.validated_data["refresh_token"] = existing_account.refresh_token @@ -106,7 +119,8 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin): ) as M: try: if ( - account.is_token + existing_account is not None + and account.is_token and account.expiration is not None and account.expiration < timezone.now() ):