From ca84c156587622274e8004ad092c542c446b24b5 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 22 Nov 2023 13:21:44 +0530 Subject: [PATCH] dev: webhook validation and fix webhook flow for app --- apiserver/plane/app/serializers/webhook.py | 42 ++++++++++++++++++++++ apiserver/plane/app/views/base.py | 12 ++++--- apiserver/plane/app/views/webhook.py | 6 ++-- apiserver/plane/db/models/webhook.py | 1 - 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/apiserver/plane/app/serializers/webhook.py b/apiserver/plane/app/serializers/webhook.py index 351b6fe7d..d5b3eeddd 100644 --- a/apiserver/plane/app/serializers/webhook.py +++ b/apiserver/plane/app/serializers/webhook.py @@ -1,3 +1,9 @@ +# Python imports +import urllib +import socket +import ipaddress +from urllib.parse import urlparse + # Third party imports from rest_framework import serializers @@ -9,6 +15,42 @@ from plane.db.models.webhook import validate_domain, validate_schema class WebhookSerializer(DynamicBaseSerializer): url = serializers.URLField(validators=[validate_schema, validate_domain]) + def validate(self, data): + url = data.get("url", None) + + # Extract the hostname from the URL + hostname = urlparse(url).hostname + if not hostname: + raise serializers.ValidationError({"url": "Invalid URL: No hostname found."}) + + # Resolve the hostname to IP addresses + try: + ip_addresses = socket.getaddrinfo(hostname, None) + except socket.gaierror: + raise serializers.ValidationError({"url": "Hostname could not be resolved."}) + + if not ip_addresses: + raise serializers.ValidationError({"url": "No IP addresses found for the hostname."}) + + for addr in ip_addresses: + ip = ipaddress.ip_address(addr[4][0]) + if ip.is_private or ip.is_loopback: + raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."}) + + # Additional validation for multiple request domains and their subdomains + request = self.context.get('request') + disallowed_domains = ['plane.so',] # Add your disallowed domains here + if request: + request_host = request.get_host().split(':')[0] # Remove port if present + disallowed_domains.append(request_host) + + # Check if hostname is a subdomain or exact match of any disallowed domain + if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains): + raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."}) + + return data + + class Meta: model = Webhook fields = "__all__" diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index de7bafd57..ba18dbbfa 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -46,22 +46,26 @@ class WebhookMixin: def finalize_response(self, request, response, *args, **kwargs): response = super().finalize_response(request, response, *args, **kwargs) - if ( self.webhook_event - and self.request.method in ["POST", "PATCH", "DELETE"] + and self.request.method in ["POST", "PATCH"] and response.status_code in [200, 201, 204] ): + # Get the id + object_id = ( + response.data.get("id") if isinstance(response.data, dict) else None + ) + send_webhook.delay( event=self.webhook_event, - event_data=json.dumps(response.data, cls=DjangoJSONEncoder), + event_id=object_id, action=self.request.method, slug=self.workspace_slug, ) - return response + class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): model = None diff --git a/apiserver/plane/app/views/webhook.py b/apiserver/plane/app/views/webhook.py index 74d23dd91..48608d583 100644 --- a/apiserver/plane/app/views/webhook.py +++ b/apiserver/plane/app/views/webhook.py @@ -20,9 +20,10 @@ class WebhookEndpoint(BaseAPIView): def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) - try: - serializer = WebhookSerializer(data=request.data) + serializer = WebhookSerializer( + data=request.data, context={"request": request} + ) if serializer.is_valid(): serializer.save(workspace_id=workspace.id) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -79,6 +80,7 @@ class WebhookEndpoint(BaseAPIView): serializer = WebhookSerializer( webhook, data=request.data, + context={request: request}, partial=True, fields=( "id", diff --git a/apiserver/plane/db/models/webhook.py b/apiserver/plane/db/models/webhook.py index 6698ec5b0..ea2b508e5 100644 --- a/apiserver/plane/db/models/webhook.py +++ b/apiserver/plane/db/models/webhook.py @@ -16,7 +16,6 @@ def generate_token(): def validate_schema(value): parsed_url = urlparse(value) - print(parsed_url) if parsed_url.scheme not in ["http", "https"]: raise ValidationError("Invalid schema. Only HTTP and HTTPS are allowed.")