CVE-2022-0645: URL Redirection to Untrusted Site ('Open Redirect') (CWE-601)
value: Open redirect vulnerability via endpoint authorize_and_redirect/?redirect= in GitHub repository posthog/posthog prior to 1.34.1.
CWE description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
CVSS v3 base score: 6.1
Published: 2022-04-19T12:15Z
Repository: https://github.com/posthog/posthog
Language: Python
Files changed: posthog/api/decide.py, posthog/test/test_urls.py, posthog/urls.py
Commit date: 2022-03-29 15:29:57 +0200
Commit message: fix(toolbar): only redirect to hosts in app_urls (#9268)

* fix(toolbar): only redirect to hosts in app_urls

* fix mypy and regex
Diff: diff --git a/posthog/api/decide.py b/posthog/api/decide.py
index 3d0aa0c4e..42d3953ea 100644
--- a/posthog/api/decide.py
+++ b/posthog/api/decide.py
@@ -18,27 +18,34 @@ from posthog.utils import cors_response, load_data_from_request
 
 from .utils import get_project_id
 
 
 def on_permitted_domain(team: Team, request: HttpRequest) -> bool:
+    origin = parse_domain(request.headers.get("Origin"))
+    referer = parse_domain(request.headers.get("Referer"))
+    return hostname_in_app_urls(team, origin) or hostname_in_app_urls(team, referer)
+
+
+def hostname_in_app_urls(team: Team, hostname: Optional[str]) -> bool:
+    if not hostname:
+        return False
+
     permitted_domains = ["127.0.0.1", "localhost"]
 
     for url in team.app_urls:
-        hostname = parse_domain(url)
-        if hostname:
-            permitted_domains.append(hostname)
+        host = parse_domain(url)
+        if host:
+            permitted_domains.append(host)
 
-    origin = parse_domain(request.headers.get("Origin"))
-    referer = parse_domain(request.headers.get("Referer"))
     for permitted_domain in permitted_domains:
         if "*" in permitted_domain:
-            pattern = "^{}$".format(permitted_domain.replace(".", "\\.").replace("*", "(.*)"))
-            if (origin and re.search(pattern, origin)) or (referer and re.search(pattern, referer)):
-                return True
-        else:
-            if permitted_domain == origin or permitted_domain == referer:
+            pattern = "^{}$".format(re.escape(permitted_domain).replace("\\*", "(.*)"))
+            if re.search(pattern, hostname):
                 return True
+        elif permitted_domain == hostname:
+            return True
+
     return False
 
 
 def decide_editor_params(request: HttpRequest) -> Tuple[Dict[str, Any], bool]:
     if request.user.is_anonymous:
diff --git a/posthog/test/test_urls.py b/posthog/test/test_urls.py
index d214621e3..4328b0a50 100644
--- a/posthog/test/test_urls.py
+++ b/posthog/test/test_urls.py
@@ -54,10 +54,19 @@ class TestUrls(APIBaseTest):
 
         response = self.client.get(f"/login")
         self.assertEqual(response.status_code, status.HTTP_200_OK)
 
     def test_authorize_and_redirect_domain(self):
+        self.team.app_urls = ["https://domain.com", "https://not.com"]
+        self.team.save()
+
+        response = self.client.get(
+            "/authorize_and_redirect/?redirect=https://not-permitted.com", HTTP_REFERER="https://not-permitted.com"
+        )
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertTrue("Can only redirect to a permitted domain." in str(response.content))
+
         response = self.client.get(
             "/authorize_and_redirect/?redirect=https://domain.com", HTTP_REFERER="https://not.com"
         )
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertTrue("Can only redirect to the same domain as the referer: not.com" in str(response.content))
diff --git a/posthog/urls.py b/posthog/urls.py
index 731a32047..1a04fb9a6 100644
--- a/posthog/urls.py
+++ b/posthog/urls.py
@@ -1,10 +1,10 @@
-from typing import Any, Callable, List, Optional
+from typing import Any, Callable, List, Optional, cast
 from urllib.parse import urlparse
 
 from django.conf import settings
-from django.http import HttpResponse
+from django.http import HttpRequest, HttpResponse
 from django.urls import URLPattern, include, path, re_path
 from django.views.decorators import csrf
 from django.views.decorators.csrf import csrf_exempt
 from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
 
@@ -19,11 +19,13 @@ from posthog.api import (
     projects_router,
     router,
     signup,
     user,
 )
+from posthog.api.decide import hostname_in_app_urls
 from posthog.demo import demo
+from posthog.models import User
 
 from .utils import render_template
 from .views import health, login_required, preflight_check, robots_txt, security_txt, sso_login, stats
 
 ee_urlpatterns: List[Any] = []
@@ -48,19 +50,23 @@ else:
 @csrf.ensure_csrf_cookie
 def home(request, *args, **kwargs):
     return render_template("index.html", request)
 
 
-def authorize_and_redirect(request):
+def authorize_and_redirect(request: HttpRequest) -> HttpResponse:
     if not request.GET.get("redirect"):
         return HttpResponse("You need to pass a url to ?redirect=", status=401)
     if not request.META.get("HTTP_REFERER"):
         return HttpResponse('You need to make a request that includes the "Referer" header.', status=400)
 
+    current_team = cast(User, request.user).team
     referer_url = urlparse(request.META["HTTP_REFERER"])
     redirect_url = urlparse(request.GET["redirect"])
 
+    if not current_team or not hostname_in_app_urls(current_team, redirect_url.hostname):
+        return HttpResponse(f"Can only redirect to a permitted domain.", status=400)
+
     if referer_url.hostname != redirect_url.hostname:
         return HttpResponse(f"Can only redirect to the same domain as the referer: {referer_url.hostname}", status=400)
 
     if referer_url.scheme != redirect_url.scheme:
         return HttpResponse(f"Can only redirect to the same scheme as the referer: {referer_url.scheme}", status=400)

Vulnerable code:         hostname = parse_domain(url)
        if hostname:
            permitted_domains.append(hostname)
    origin = parse_domain(request.headers.get("Origin"))
    referer = parse_domain(request.headers.get("Referer"))
            pattern = "^{}$".format(permitted_domain.replace(".", "\\.").replace("*", "(.*)"))
            if (origin and re.search(pattern, origin)) or (referer and re.search(pattern, referer)):
                return True
        else:
            if permitted_domain == origin or permitted_domain == referer:
from typing import Any, Callable, List, Optional
from django.http import HttpResponse
def authorize_and_redirect(request):
Fixed code:     origin = parse_domain(request.headers.get("Origin"))
    referer = parse_domain(request.headers.get("Referer"))
    return hostname_in_app_urls(team, origin) or hostname_in_app_urls(team, referer)


def hostname_in_app_urls(team: Team, hostname: Optional[str]) -> bool:
    if not hostname:
        return False

        host = parse_domain(url)
        if host:
            permitted_domains.append(host)
            pattern = "^{}$".format(re.escape(permitted_domain).replace("\\*", "(.*)"))
            if re.search(pattern, hostname):
        elif permitted_domain == hostname:
            return True

        self.team.app_urls = ["https://domain.com", "https://not.com"]
        self.team.save()

        response = self.client.get(
            "/authorize_and_redirect/?redirect=https://not-permitted.com", HTTP_REFERER="https://not-permitted.com"
        )
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertTrue("Can only redirect to a permitted domain." in str(response.content))

from typing import Any, Callable, List, Optional, cast
from django.http import HttpRequest, HttpResponse
from posthog.api.decide import hostname_in_app_urls
from posthog.models import User
def authorize_and_redirect(request: HttpRequest) -> HttpResponse:
    current_team = cast(User, request.user).team
    if not current_team or not hostname_in_app_urls(current_team, redirect_url.hostname):
        return HttpResponse(f"Can only redirect to a permitted domain.", status=400)