From c4b3d5246643d93eeaccbf3ee77223d3d53e5639 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 17 Feb 2026 00:49:02 +0530 Subject: [PATCH] [WEB-5878] chore: add validation for project name/identifier for special characters (#8529) * chore: update ProjectSerializer to raise validation for special characters in name and identifier * chore: update external endpoints * fix: external api serializer validation * update serializer to send error code * fix: move the regex expression to Project model --- apps/api/plane/api/serializers/project.py | 31 +++++++++++++++++++++++ apps/api/plane/app/serializers/project.py | 9 +++++++ apps/api/plane/db/models/project.py | 2 ++ 3 files changed, 42 insertions(+) diff --git a/apps/api/plane/api/serializers/project.py b/apps/api/plane/api/serializers/project.py index 01ccf9434d..644b5ba107 100644 --- a/apps/api/plane/api/serializers/project.py +++ b/apps/api/plane/api/serializers/project.py @@ -6,6 +6,10 @@ import random from rest_framework import serializers + +# Python imports +import re + # Module imports from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate @@ -101,6 +105,15 @@ class ProjectCreateSerializer(BaseSerializer): ] def validate(self, data): + project_name = data.get("name", None) + project_identifier = data.get("identifier", None) + + if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name): + raise serializers.ValidationError("Project name cannot contain special characters.") + + if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier): + raise serializers.ValidationError("Project identifier cannot contain special characters.") + if data.get("project_lead", None) is not None: # Check if the project lead is a member of the workspace if not WorkspaceMember.objects.filter( @@ -160,6 +173,15 @@ class ProjectUpdateSerializer(ProjectCreateSerializer): read_only_fields = ProjectCreateSerializer.Meta.read_only_fields def update(self, instance, validated_data): + project_name = validated_data.get("name", None) + project_identifier = validated_data.get("identifier", None) + + if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name): + raise serializers.ValidationError("Project name cannot contain special characters.") + + if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier): + raise serializers.ValidationError("Project identifier cannot contain special characters.") + """Update a project""" if ( validated_data.get("default_state", None) is not None @@ -210,6 +232,15 @@ class ProjectSerializer(BaseSerializer): ] def validate(self, data): + project_name = data.get("name", None) + project_identifier = data.get("identifier", None) + + if project_name is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_name): + raise serializers.ValidationError("Project name cannot contain special characters.") + + if project_identifier is not None and re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, project_identifier): + raise serializers.ValidationError("Project identifier cannot contain special characters.") + # Check project lead should be a member of the workspace if ( data.get("project_lead", None) is not None diff --git a/apps/api/plane/app/serializers/project.py b/apps/api/plane/app/serializers/project.py index 77d7dc658e..924c48fcfa 100644 --- a/apps/api/plane/app/serializers/project.py +++ b/apps/api/plane/app/serializers/project.py @@ -5,6 +5,9 @@ # Third party imports from rest_framework import serializers +# Python imports +import re + # Module imports from .base import BaseSerializer, DynamicBaseSerializer from django.db.models import Max @@ -37,6 +40,9 @@ class ProjectSerializer(BaseSerializer): project_id = self.instance.id if self.instance else None workspace_id = self.context["workspace_id"] + if re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, name): + raise serializers.ValidationError(detail="PROJECT_NAME_CANNOT_CONTAIN_SPECIAL_CHARACTERS") + project = Project.objects.filter(name=name, workspace_id=workspace_id) if project_id: @@ -53,6 +59,9 @@ class ProjectSerializer(BaseSerializer): project_id = self.instance.id if self.instance else None workspace_id = self.context["workspace_id"] + if re.match(Project.FORBIDDEN_IDENTIFIER_CHARS_PATTERN, identifier): + raise serializers.ValidationError(detail="PROJECT_IDENTIFIER_CANNOT_CONTAIN_SPECIAL_CHARACTERS") + project = Project.objects.filter(identifier=identifier, workspace_id=workspace_id) if project_id: diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 148bd08fc6..4039b1d290 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -140,6 +140,8 @@ class Project(BaseModel): """Return name of the project""" return f"{self.name} <{self.workspace.name}>" + FORBIDDEN_IDENTIFIER_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!-].*$" + class Meta: unique_together = [ ["identifier", "workspace", "deleted_at"],