From edff92211d713347260d42d513eff3172fd69a4b Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 6 Sep 2026 08:51:41 +0200 Subject: [PATCH 1/2] improve(auth): hide email exist response --- backend/tests/user/api/test_user_viewsets.py | 18 ++++++++++++++++++ backend/user/api/serializer.py | 9 ++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/backend/tests/user/api/test_user_viewsets.py b/backend/tests/user/api/test_user_viewsets.py index 0298ea9..04b2015 100644 --- a/backend/tests/user/api/test_user_viewsets.py +++ b/backend/tests/user/api/test_user_viewsets.py @@ -138,6 +138,24 @@ def test_register_rejects_duplicate_username(self, api_client, user_factory): response = api_client.post(reverse('user:user_signup'), data=self._payload(), format='json') assert response.status_code == 400 + def test_register_silently_drops_duplicate_email(self, api_client, user_factory): + owner = user_factory(username='original_owner', email='newpilot@example.com') + + with patch('user.tasks.send_email_verification_code.apply_async') as send_code: + response = api_client.post( + reverse('user:user_signup'), data=self._payload(username='second_pilot'), format='json' + ) + + assert response.status_code == 201 + assert response.data['username'] == 'second_pilot' + send_code.assert_not_called() + + user = User.objects.get(username='second_pilot') + assert user.email is None + + owner.refresh_from_db() + assert owner.email == 'newpilot@example.com' + class TestUserAPIKeyViewSet: def test_list_requires_auth(self, api_client): diff --git a/backend/user/api/serializer.py b/backend/user/api/serializer.py index ebae601..e6020e5 100644 --- a/backend/user/api/serializer.py +++ b/backend/user/api/serializer.py @@ -86,11 +86,10 @@ def validate_username(self, value): return value def validate_email(self, value): - # Only check uniqueness if an email was actually provided - if value: - # Case-insensitive check - if User.objects.filter(email__iexact=value).exists(): - raise serializers.ValidationError('A user with this email already exists.') + # Don't reveal whether an email is already registered (account enumeration): + # silently drop it so registration still succeeds, just without that email attached. + if value and User.objects.filter(email__iexact=value).exists(): + return None return value def validate(self, attrs): From 8bc47e03b5403a3f9e402d9e1333620a7f8b29dc Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 6 Sep 2026 08:54:34 +0200 Subject: [PATCH 2/2] fix(test): auth message hide --- backend/tests/user/api/test_user_viewsets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/tests/user/api/test_user_viewsets.py b/backend/tests/user/api/test_user_viewsets.py index 04b2015..ff5ea99 100644 --- a/backend/tests/user/api/test_user_viewsets.py +++ b/backend/tests/user/api/test_user_viewsets.py @@ -139,16 +139,16 @@ def test_register_rejects_duplicate_username(self, api_client, user_factory): assert response.status_code == 400 def test_register_silently_drops_duplicate_email(self, api_client, user_factory): - owner = user_factory(username='original_owner', email='newpilot@example.com') - with patch('user.tasks.send_email_verification_code.apply_async') as send_code: + owner = user_factory(username='original_owner', email='newpilot@example.com') response = api_client.post( reverse('user:user_signup'), data=self._payload(username='second_pilot'), format='json' ) assert response.status_code == 201 assert response.data['username'] == 'second_pilot' - send_code.assert_not_called() + # only the owner's own creation should have queued a code, not the second registration + send_code.assert_called_once() user = User.objects.get(username='second_pilot') assert user.email is None