فهرست منبع

Merge pull request #21 from desec-io/20170103_captcha_ex

feat(api): extended abuse protection
Peter Thomassen 8 سال پیش
والد
کامیت
6c2d85ae0b

+ 1 - 1
api/desecapi/management/commands/privacy-chores.py

@@ -9,7 +9,7 @@ class Command(BaseCommand):
 
     def handle(self, *args, **kwargs):
 
-        users = User.objects.filter(created__lt=timezone.now()-timedelta(hours=settings.ABUSE_LOCK_ACCOUNT_BY_REGISTRATION_IP_PERIOD_HRS))
+        users = User.objects.filter(created__lt=timezone.now()-timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS))
         for u in users:
             u.registration_remote_ip = ''
             u.save() # TODO bulk update?

+ 8 - 2
api/desecapi/pdns.py

@@ -31,7 +31,7 @@ def _pdns_patch(url, body):
 
 def _pdns_get(url):
     r = requests.get(settings.POWERDNS_API + url, headers=headers)
-    if (r.status_code < 200 or r.status_code >= 300) and r.status_code != 404:
+    if (r.status_code < 200 or r.status_code >= 500):
         raise Exception(r.text)
     return r
 
@@ -85,7 +85,13 @@ def zone_exists(name):
     """
     Returns whether pdns knows a zone with the given name.
     """
-    return _pdns_get('/zones/' + normalize_hostname(name)).status_code != 404
+    reply = _pdns_get('/zones/' + normalize_hostname(name))
+    if reply.status_code == 200:
+        return True
+    elif reply.status_code == 422 and 'Could not find domain' in reply.text:
+        return False
+    else:
+        raise Exception(reply.text)
 
 
 def set_dyn_records(name, a, aaaa):

+ 4 - 1
api/desecapi/settings.py

@@ -155,5 +155,8 @@ NORECAPTCHA_SECRET_KEY = os.environ['DESECSTACK_NORECAPTCHA_SECRET_KEY']
 NORECAPTCHA_WIDGET_TEMPLATE = 'captcha-widget.html'
 
 # abuse protection
-ABUSE_LOCK_ACCOUNT_BY_REGISTRATION_IP_PERIOD_HRS = 48
+ABUSE_BY_REMOTE_IP_LIMIT = 1
+ABUSE_BY_REMOTE_IP_PERIOD_HRS = 48
+ABUSE_BY_EMAIL_HOSTNAME_LIMIT = 1
+ABUSE_BY_EMAIL_HOSTNAME_PERIOD_HRS = 24
 LIMIT_USER_DOMAIN_COUNT_DEFAULT = 5

+ 1 - 1
api/desecapi/tests/testprivacychores.py

@@ -22,7 +22,7 @@ class PrivacyChoresCommandTest(TestCase):
             registration_remote_ip='1.3.3.8',
         ).save()
         user2 = User.objects.get(email=name2)
-        user2.created = timezone.now()-timedelta(hours=settings.ABUSE_LOCK_ACCOUNT_BY_REGISTRATION_IP_PERIOD_HRS+1)
+        user2.created = timezone.now()-timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS+1)
         user2.save()
 
         user_count = User.objects.all().count()

+ 56 - 1
api/desecapi/tests/testregistration.py

@@ -87,7 +87,7 @@ class RegistrationTest(APITestCase):
         self.assertEqual(user.captcha_required, False)
 
         #fake registration time
-        user.created = timezone.now() - timedelta(hours=settings.ABUSE_LOCK_ACCOUNT_BY_REGISTRATION_IP_PERIOD_HRS+1)
+        user.created = timezone.now() - timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS+1)
         user.save()
 
         url = reverse('register')
@@ -110,3 +110,58 @@ class RegistrationTest(APITestCase):
         send_account_lock_email(None, user)
 
         self.assertEqual(len(mail.outbox), outboxlen+1)
+
+    def test_multiple_registration_captcha_required_same_email_host(self):
+        outboxlen = len(mail.outbox)
+
+        url = reverse('register')
+        for i in range(settings.ABUSE_BY_EMAIL_HOSTNAME_LIMIT):
+            data = {'email': utils.generateRandomString() + '@test-same-email.desec.io', 'password': utils.generateRandomString(size=12)}
+            response = self.client.post(url, data, REMOTE_ADDR=utils.generateRandomString())
+            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+            user = models.User.objects.get(email=data['email'])
+            self.assertEqual(user.email, data['email'])
+            self.assertEqual(user.captcha_required, False)
+
+        self.assertEqual(len(mail.outbox), outboxlen)
+
+        url = reverse('register')
+        data = {'email': utils.generateRandomString() + '@test-same-email.desec.io',
+                'password': utils.generateRandomString(size=12)}
+        response = self.client.post(url, data, REMOTE_ADDR=utils.generateRandomString())
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        user = models.User.objects.get(email=data['email'])
+        self.assertEqual(user.email, data['email'])
+        self.assertEqual(user.captcha_required, True)
+
+        self.assertEqual(len(mail.outbox), outboxlen + 1)
+
+    def test_multiple_registration_no_captcha_required_same_email_host_long_time(self):
+        outboxlen = len(mail.outbox)
+
+        url = reverse('register')
+        for i in range(settings.ABUSE_BY_EMAIL_HOSTNAME_LIMIT):
+            data = {'email': utils.generateRandomString() + '@test-same-email-1.desec.io', 'password': utils.generateRandomString(size=12)}
+            response = self.client.post(url, data, REMOTE_ADDR=utils.generateRandomString())
+            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+            user = models.User.objects.get(email=data['email'])
+            self.assertEqual(user.email, data['email'])
+            self.assertEqual(user.captcha_required, False)
+
+            #fake registration time
+            user = models.User.objects.get(email=data['email'])
+            user.created = timezone.now() - timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS+1)
+            user.save()
+
+        self.assertEqual(len(mail.outbox), outboxlen)
+
+        url = reverse('register')
+        data = {'email': utils.generateRandomString() + '@test-same-email-1.desec.io',
+                'password': utils.generateRandomString(size=12)}
+        response = self.client.post(url, data, REMOTE_ADDR=utils.generateRandomString())
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        user = models.User.objects.get(email=data['email'])
+        self.assertEqual(user.email, data['email'])
+        self.assertEqual(user.captcha_required, False)
+
+        self.assertEqual(len(mail.outbox), outboxlen)

+ 1 - 1
api/desecapi/tests/utils.py

@@ -12,7 +12,7 @@ class utils(object):
 
     @classmethod
     def generateUsername(cls):
-        return cls.generateRandomString() + '@desec.io'
+        return cls.generateRandomString() + '@' + cls.generateRandomString() + 'desec.io'
 
     @classmethod
     def generateDomainname(cls):

+ 14 - 10
api/desecapi/views.py

@@ -29,12 +29,7 @@ from desecapi.emails import send_account_lock_email
 
 
 def get_client_ip(request):
-    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
-    if x_forwarded_for:
-        ip = x_forwarded_for.split(',')[0]
-    else:
-        ip = request.META.get('REMOTE_ADDR')
-    return ip
+    return request.META.get('REMOTE_ADDR')
 
 
 class DomainList(generics.ListCreateAPIView):
@@ -295,10 +290,19 @@ class RegistrationView(views.RegistrationView):
         return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
 
     def perform_create(self, serializer, remote_ip):
-        captcha = User.objects.filter(
-                created__gte=timezone.now()-timedelta(hours=settings.ABUSE_LOCK_ACCOUNT_BY_REGISTRATION_IP_PERIOD_HRS),
-                registration_remote_ip=remote_ip
-            ).exists()
+        captcha = \
+            (
+                User.objects.filter(
+                    created__gte=timezone.now()-timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS),
+                    registration_remote_ip=remote_ip
+                ).count() >= settings.ABUSE_BY_REMOTE_IP_LIMIT
+                or
+                User.objects.filter(
+                    created__gte=timezone.now() - timedelta(hours=settings.ABUSE_BY_EMAIL_HOSTNAME_PERIOD_HRS),
+                    email__endswith=serializer.validated_data['email'].split('@')[-1]
+                ).count() >= settings.ABUSE_BY_EMAIL_HOSTNAME_LIMIT
+            )
+
         user = serializer.save(registration_remote_ip=remote_ip, captcha_required=captcha)
         if captcha:
             send_account_lock_email(self.request, user)

+ 1 - 0
docker-compose.dev.yml

@@ -32,6 +32,7 @@ services:
     ports:
      - "5311:53"
      - "5311:53/udp"
+     - "127.0.0.1:8081:8081"
     logging:
       driver: "json-file"