Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions apiserver/plane/app/views/external/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import List, Dict, Tuple

# Third party import
import litellm
from openai import OpenAI
import requests

from rest_framework import status
Expand Down Expand Up @@ -116,12 +116,14 @@ def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> T
if provider.lower() == "gemini":
model = f"gemini/{model}"

response = litellm.completion(
client = OpenAI(api_key=api_key)
chat_completion = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": final_text}],
api_key=api_key,
messages=[
{"role": "user", "content": final_text}
]
)
text = response.choices[0].message.content.strip()
text = chat_completion.choices[0].message.content
return text, None
except Exception as e:
log_exception(e)
Expand Down Expand Up @@ -175,7 +177,7 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
api_key, model, provider = get_llm_config()

if not api_key or not model or not provider:
return Response(
{"error": "LLM provider API key and model are required"},
Expand Down
7 changes: 5 additions & 2 deletions apiserver/plane/app/views/intake/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,17 @@ def get_queryset(self):

@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
intake_id = Intake.objects.filter(
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
if not intake:
return Response({"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND)

project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__")
intake_issue = (
IntakeIssue.objects.filter(
intake_id=intake_id.id, project_id=project_id, **filters
intake_id=intake.id, project_id=project_id, **filters
)
.select_related("issue")
.prefetch_related("issue__labels")
Expand Down
20 changes: 17 additions & 3 deletions apiserver/plane/app/views/issue/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ def retrieve(self, request, slug, project_id, pk=None):
)

"""
if the role is guest and guest_view_all_features is false and owned by is not
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the issue
"""

Expand Down Expand Up @@ -1116,8 +1116,22 @@ def get(self, request, slug, project_id, issue_id):

class IssueDetailIdentifierEndpoint(BaseAPIView):

def strict_str_to_int(self, s):
if not s.isdigit() and not (s.startswith('-') and s[1:].isdigit()):
raise ValueError("Invalid integer string")
return int(s)

def get(self, request, slug, project_identifier, issue_identifier):


# Check if the issue identifier is a valid integer
try:
issue_identifier = self.strict_str_to_int(issue_identifier)
except ValueError:
return Response(
{"error": "Invalid issue identifier"},
status=status.HTTP_400_BAD_REQUEST,
)

# Fetch the project
project = Project.objects.get(
identifier__iexact=project_identifier,
Expand Down Expand Up @@ -1240,7 +1254,7 @@ def get(self, request, slug, project_identifier, issue_identifier):
)

"""
if the role is guest and guest_view_all_features is false and owned by is not
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the issue
"""

Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/module/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def create_issue_modules(self, request, slug, project_id, issue_id):
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
{"module_name": module_issue.first().module.name}
{"module_name": module_issue.first().module.name if (module_issue.first() and module_issue.first().module) else None}
),
epoch=int(timezone.now().timestamp()),
notification=True,
Expand Down
16 changes: 14 additions & 2 deletions apiserver/plane/authentication/views/app/password_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,20 @@ class ResetPasswordEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
try:
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
except (ValueError, User.DoesNotExist):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD_TOKEN"],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request, is_app=True),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)

Check warning

Code scanning / CodeQL

URL redirection from remote source

Untrusted URL redirection depends on a [user-provided value](1).

Copilot Autofix

AI over 1 year ago

To fix the problem, we need to ensure that the URL used in the HttpResponseRedirect function is safe and not influenced by untrusted user input. We can achieve this by validating the constructed URL before using it for redirection. Specifically, we can use the url_has_allowed_host_and_scheme function from Django to check that the URL is safe.

  1. Import the url_has_allowed_host_and_scheme function from django.utils.http.
  2. Validate the constructed URL using url_has_allowed_host_and_scheme before redirecting.
Suggested changeset 1
apiserver/plane/authentication/views/app/password_management.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apiserver/plane/authentication/views/app/password_management.py b/apiserver/plane/authentication/views/app/password_management.py
--- a/apiserver/plane/authentication/views/app/password_management.py
+++ b/apiserver/plane/authentication/views/app/password_management.py
@@ -17,3 +17,3 @@
 from django.utils.encoding import DjangoUnicodeDecodeError, smart_bytes, smart_str
-from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
+from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode, url_has_allowed_host_and_scheme
 from django.views import View
@@ -111,7 +111,11 @@
                 params = exc.get_error_dict()
+                base_url = base_host(request=request, is_app=True)
                 url = urljoin(
-                    base_host(request=request, is_app=True),
+                    base_url,
                     "accounts/reset-password?" + urlencode(params),
                 )
-                return HttpResponseRedirect(url)
+                if url_has_allowed_host_and_scheme(url, allowed_hosts={base_url}):
+                    return HttpResponseRedirect(url)
+                else:
+                    return HttpResponseRedirect('/')
 
@@ -124,7 +128,11 @@
                 params = exc.get_error_dict()
+                base_url = base_host(request=request, is_app=True)
                 url = urljoin(
-                    base_host(request=request, is_app=True),
+                    base_url,
                     "accounts/reset-password?" + urlencode(params),
                 )
-                return HttpResponseRedirect(url)
+                if url_has_allowed_host_and_scheme(url, allowed_hosts={base_url}):
+                    return HttpResponseRedirect(url)
+                else:
+                    return HttpResponseRedirect('/')
 
@@ -137,7 +145,11 @@
                 )
+                base_url = base_host(request=request, is_app=True)
                 url = urljoin(
-                    base_host(request=request, is_app=True),
+                    base_url,
                     "accounts/reset-password?" + urlencode(exc.get_error_dict()),
                 )
-                return HttpResponseRedirect(url)
+                if url_has_allowed_host_and_scheme(url, allowed_hosts={base_url}):
+                    return HttpResponseRedirect(url)
+                else:
+                    return HttpResponseRedirect('/')
 
@@ -150,7 +162,11 @@
                 )
+                base_url = base_host(request=request, is_app=True)
                 url = urljoin(
-                    base_host(request=request, is_app=True),
+                    base_url,
                     "accounts/reset-password?" + urlencode(exc.get_error_dict()),
                 )
-                return HttpResponseRedirect(url)
+                if url_has_allowed_host_and_scheme(url, allowed_hosts={base_url}):
+                    return HttpResponseRedirect(url)
+                else:
+                    return HttpResponseRedirect('/')
 
@@ -161,7 +177,11 @@
 
+            base_url = base_host(request=request, is_app=True)
             url = urljoin(
-                base_host(request=request, is_app=True),
+                base_url,
                 "sign-in?" + urlencode({"success": True}),
             )
-            return HttpResponseRedirect(url)
+            if url_has_allowed_host_and_scheme(url, allowed_hosts={base_url}):
+                return HttpResponseRedirect(url)
+            else:
+                return HttpResponseRedirect('/')
         except DjangoUnicodeDecodeError:
@@ -171,6 +191,10 @@
             )
+            base_url = base_host(request=request, is_app=True)
             url = urljoin(
-                base_host(request=request, is_app=True),
+                base_url,
                 "accounts/reset-password?" + urlencode(exc.get_error_dict()),
             )
-            return HttpResponseRedirect(url)
+            if url_has_allowed_host_and_scheme(url, allowed_hosts={base_url}):
+                return HttpResponseRedirect(url)
+            else:
+                return HttpResponseRedirect('/')
EOF
Copilot is powered by AI and may make mistakes. Always verify output.

# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
Expand Down
11 changes: 8 additions & 3 deletions apiserver/plane/bgtasks/issue_activities_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone

from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications

# Module imports
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
from plane.db.models import (
CommentReaction,
Cycle,
Expand All @@ -32,7 +32,7 @@
from plane.utils.exception_logger import log_exception
from plane.bgtasks.webhook_task import webhook_activity
from plane.utils.issue_relation_mapper import get_inverse_relation

from plane.utils.valid_uuid import is_valid_uuid

# Track Changes in name
def track_name(
Expand Down Expand Up @@ -1568,9 +1568,14 @@ def issue_activity(
try:
issue_activities = []

# check if project_id is valid
if not is_valid_uuid(project_id):
return

project = Project.objects.get(pk=project_id)
workspace_id = project.workspace_id


if issue_id is not None:
if origin:
ri = redis_instance()
Expand Down
8 changes: 8 additions & 0 deletions apiserver/plane/utils/valid_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import uuid

def is_valid_uuid(uuid_str):
try:
uuid.UUID(uuid_str, version=4)
return True
except ValueError:
return False
6 changes: 3 additions & 3 deletions apiserver/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Django==4.2.18
# rest framework
djangorestframework==3.15.2
# postgres
# postgres
psycopg==3.1.18
psycopg-binary==3.1.18
psycopg-c==3.1.18
Expand Down Expand Up @@ -37,7 +37,7 @@ uvicorn==0.29.0
# sockets
channels==4.1.0
# ai
litellm==1.51.0
openai==1.63.2
# slack
slack-sdk==3.27.1
# apm
Expand Down Expand Up @@ -66,4 +66,4 @@ PyJWT==2.8.0
opentelemetry-api==1.28.1
opentelemetry-sdk==1.28.1
opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
opentelemetry-exporter-otlp==1.28.1
2 changes: 1 addition & 1 deletion apiserver/requirements/local.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# debug toolbar
django-debug-toolbar==4.3.0
# formatter
ruff==0.4.2
ruff==0.9.7