CVE-2024-31461: Insufficient Information (NVD-CWE-noinfo)
value: Plane, an open-source project management tool, has a Server-Side Request Forgery (SSRF) vulnerability in versions prior to 0.17-dev. This issue may allow an attacker to send arbitrary requests from the server hosting the application, potentially leading to unauthorized access to internal systems. The impact of this vulnerability includes, but is not limited to, unauthorized access to internal services accessible from the server, potential leakage of sensitive information from internal services, manipulation of internal systems by interacting with internal APIs. Version 0.17-dev contains a patch for this issue. Those who are unable to update immediately may mitigate the issue by restricting outgoing network connections from servers hosting the application to essential services only and/or implementing strict input validation on URLs or parameters that are used to generate server-side requests.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-10T18:15Z
Repository: https://github.com/makeplane/plane
Language: Python
Files changed: apiserver/plane/utils/importers/jira.py
Commit date: 2024-01-09 20:40:23 +0530
Commit message: fix: update jira summary endpoints (#3333)

* dev: update jira summary endpoints

* dev: update jira project key validations

* dev: updated key length
Diff: diff --git a/apiserver/plane/utils/importers/jira.py b/apiserver/plane/utils/importers/jira.py
index 3081096fe..5e8c31f97 100644
--- a/apiserver/plane/utils/importers/jira.py
+++ b/apiserver/plane/utils/importers/jira.py
@@ -1,55 +1,91 @@
 import requests
+import re
 from requests.auth import HTTPBasicAuth
 from sentry_sdk import capture_exception
+from urllib.parse import urlparse, urljoin
 
-from urllib.parse import urlparse
 
 def is_allowed_hostname(hostname):
-    allowed_lists = ["atl-paas.net", "atlassian.com", "atlassian.net", "jira.com"]
-    # Extract the base domain from the hostname
-    parsed_uri = urlparse(f"https://{hostname}")  # Add scheme for urlparse to work properly
-    domain = parsed_uri.netloc.split(":")[0]  # Removes port number if included
-    base_domain = ".".join(domain.split(".")[-2:])  # Extract base domain
+    allowed_domains = ["atl-paas.net", "atlassian.com", "atlassian.net", "jira.com"]
+    parsed_uri = urlparse(f"https://{hostname}")
+    domain = parsed_uri.netloc.split(":")[0]  # Ensures no port is included
+    base_domain = ".".join(domain.split(".")[-2:])
+    return base_domain in allowed_domains
 
-    # Check if the base domain is in the allowed list
-    return base_domain in allowed_lists
+
+def is_valid_project_key(project_key):
+    if project_key:
+        project_key = project_key.strip().upper()
+        # Adjust the regular expression as needed based on your specific requirements.
+        if len(project_key) > 30:
+            return False
+        # Check the validity of the key as well
+        pattern = re.compile(r'^[A-Z0-9]{1,10}$')
+        return pattern.match(project_key) is not None
+    else:
+        False
+
+def generate_valid_project_key(project_key):
+    return project_key.strip().upper()
+
+def generate_url(hostname, path):
+    if not is_allowed_hostname(hostname):
+        raise ValueError("Invalid or unauthorized hostname")
+    return urljoin(f"https://{hostname}", path)
 
 
 def jira_project_issue_summary(email, api_token, project_key, hostname):
     try:
-        
-
         if not is_allowed_hostname(hostname):
-            print("Errored Hostname")
             return {"error": "Invalid or unauthorized hostname"}
 
+        if not is_valid_project_key(project_key):
+            return {"error": "Invalid project key"}
+
         auth = HTTPBasicAuth(email, api_token)
         headers = {"Accept": "application/json"}
+        
+        # make the project key upper case
+        project_key = generate_valid_project_key(project_key)
 
-        issue_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic"
+        # issues
+        issue_url = generate_url(
+            hostname,
+            f"/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic",
+        )
         issue_response = requests.request(
             "GET", issue_url, headers=headers, auth=auth
         ).json()["total"]
 
-        module_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Epic"
+        # modules
+        module_url = generate_url(
+            hostname, f"/rest/api/3/search?jql=project={project_key} AND issuetype=Epic"
+        )
         module_response = requests.request(
             "GET", module_url, headers=headers, auth=auth
         ).json()["total"]
 
-        status_url = f"https://{hostname}/rest/api/3/project/${project_key}/statuses"
+        # status
+        status_url = generate_url(
+            hostname, f"/rest/api/3/project/${project_key}/statuses"
+        )
         status_response = requests.request(
             "GET", status_url, headers=headers, auth=auth
         ).json()
 
-        labels_url = f"https://{hostname}/rest/api/3/label/?jql=project={project_key}"
+        # labels
+        labels_url = generate_url(
+            hostname, f"/rest/api/3/label/?jql=project={project_key}"
+        )
         labels_response = requests.request(
             "GET", labels_url, headers=headers, auth=auth
         ).json()["total"]
 
-        users_url = (
-            f"https://{hostname}/rest/api/3/users/search?jql=project={project_key}"
+        # users
+        users_url = generate_url(
+            hostname, f"/rest/api/3/users/search?jql=project={project_key}"
         )
         users_response = requests.request(
             "GET", users_url, headers=headers, auth=auth
         ).json()
 

Vulnerable code: from urllib.parse import urlparse
    allowed_lists = ["atl-paas.net", "atlassian.com", "atlassian.net", "jira.com"]
    # Extract the base domain from the hostname
    parsed_uri = urlparse(f"https://{hostname}")  # Add scheme for urlparse to work properly
    domain = parsed_uri.netloc.split(":")[0]  # Removes port number if included
    base_domain = ".".join(domain.split(".")[-2:])  # Extract base domain
    # Check if the base domain is in the allowed list
    return base_domain in allowed_lists
        

            print("Errored Hostname")
        issue_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic"
        module_url = f"https://{hostname}/rest/api/3/search?jql=project={project_key} AND issuetype=Epic"
        status_url = f"https://{hostname}/rest/api/3/project/${project_key}/statuses"
        labels_url = f"https://{hostname}/rest/api/3/label/?jql=project={project_key}"
        users_url = (
            f"https://{hostname}/rest/api/3/users/search?jql=project={project_key}"
Fixed code: import re
from urllib.parse import urlparse, urljoin
    allowed_domains = ["atl-paas.net", "atlassian.com", "atlassian.net", "jira.com"]
    parsed_uri = urlparse(f"https://{hostname}")
    domain = parsed_uri.netloc.split(":")[0]  # Ensures no port is included
    base_domain = ".".join(domain.split(".")[-2:])
    return base_domain in allowed_domains

def is_valid_project_key(project_key):
    if project_key:
        project_key = project_key.strip().upper()
        # Adjust the regular expression as needed based on your specific requirements.
        if len(project_key) > 30:
            return False
        # Check the validity of the key as well
        pattern = re.compile(r'^[A-Z0-9]{1,10}$')
        return pattern.match(project_key) is not None
    else:
        False

def generate_valid_project_key(project_key):
    return project_key.strip().upper()

def generate_url(hostname, path):
    if not is_allowed_hostname(hostname):
        raise ValueError("Invalid or unauthorized hostname")
    return urljoin(f"https://{hostname}", path)
        if not is_valid_project_key(project_key):
            return {"error": "Invalid project key"}

        
        # make the project key upper case
        project_key = generate_valid_project_key(project_key)
        # issues
        issue_url = generate_url(
            hostname,
            f"/rest/api/3/search?jql=project={project_key} AND issuetype!=Epic",
        )
        # modules
        module_url = generate_url(
            hostname, f"/rest/api/3/search?jql=project={project_key} AND issuetype=Epic"
        )
        # status
        status_url = generate_url(
            hostname, f"/rest/api/3/project/${project_key}/statuses"
        )
        # labels
        labels_url = generate_url(
            hostname, f"/rest/api/3/label/?jql=project={project_key}"
        )
        # users
        users_url = generate_url(
            hostname, f"/rest/api/3/users/search?jql=project={project_key}"