From 2aa74acb126020f542e22a459d098201e1f5d2ca Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 12 Jan 2026 14:37:32 +0530 Subject: [PATCH 1/5] chore: update ProjectSerializer to raise validation for special characters in name and identifier --- apps/api/plane/app/serializers/project.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/api/plane/app/serializers/project.py b/apps/api/plane/app/serializers/project.py index b8a4136c962..8cb994b045a 100644 --- a/apps/api/plane/app/serializers/project.py +++ b/apps/api/plane/app/serializers/project.py @@ -1,6 +1,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 @@ -19,6 +22,11 @@ validate_html_content, ) +# Regex pattern to block the following special characters in names and identifiers: +# & + , : ; $ ^ } { * = ? @ # | ' < > . ( ) % ! + +FORBIDDEN_NAME_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!].*$" + class ProjectSerializer(BaseSerializer): workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) @@ -33,6 +41,9 @@ def validate_name(self, name): project_id = self.instance.id if self.instance else None workspace_id = self.context["workspace_id"] + if re.match(FORBIDDEN_NAME_CHARS_PATTERN, name): + raise serializers.ValidationError("Project name cannot contain special characters.") + project = Project.objects.filter(name=name, workspace_id=workspace_id) if project_id: @@ -49,6 +60,9 @@ def validate_identifier(self, identifier): project_id = self.instance.id if self.instance else None workspace_id = self.context["workspace_id"] + if re.match(FORBIDDEN_NAME_CHARS_PATTERN, identifier): + raise serializers.ValidationError("Project identifier cannot contain special characters.") + project = Project.objects.filter(identifier=identifier, workspace_id=workspace_id) if project_id: From aa582b7ea4278c9c291f41c7bc40942cefe1a289 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 12 Jan 2026 15:07:40 +0530 Subject: [PATCH 2/5] chore: update external endpoints --- apps/api/plane/api/serializers/project.py | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/apps/api/plane/api/serializers/project.py b/apps/api/plane/api/serializers/project.py index c9a0a31c89b..9ddb78ee516 100644 --- a/apps/api/plane/api/serializers/project.py +++ b/apps/api/plane/api/serializers/project.py @@ -2,6 +2,10 @@ import random from rest_framework import serializers + +# Python imports +import re + # Module imports from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate @@ -10,6 +14,11 @@ ) from .base import BaseSerializer +# Regex pattern to block the following special characters in names and identifiers: +# & + , : ; $ ^ } { * = ? @ # | ' < > . ( ) % ! + +FORBIDDEN_NAME_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!].*$" + class ProjectCreateSerializer(BaseSerializer): """ @@ -97,6 +106,15 @@ class Meta: ] def validate(self, data): + project_name = data.get("name", None) is not None + project_identifier = data.get("identifier", None) is not None + + if project_name and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + raise serializers.ValidationError("Project name cannot contain special characters.") + + if project_identifier and re.match(FORBIDDEN_NAME_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( @@ -156,6 +174,15 @@ class Meta(ProjectCreateSerializer.Meta): read_only_fields = ProjectCreateSerializer.Meta.read_only_fields def update(self, instance, validated_data): + project_name = validated_data.get("name", None) is not None + project_identifier = validated_data.get("identifier", None) is not None + + if project_name and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + raise serializers.ValidationError("Project name cannot contain special characters.") + + if project_identifier and re.match(FORBIDDEN_NAME_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 @@ -206,6 +233,15 @@ class Meta: ] def validate(self, data): + project_name = data.get("name", None) is not None + project_identifier = data.get("identifier", None) is not None + + if project_name and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + raise serializers.ValidationError("Project name cannot contain special characters.") + + if project_identifier and re.match(FORBIDDEN_NAME_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 From 818bec1e5477f091641ec600859f0ac6a134793d Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 12 Jan 2026 15:19:32 +0530 Subject: [PATCH 3/5] fix: external api serializer validation --- apps/api/plane/api/serializers/project.py | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/api/plane/api/serializers/project.py b/apps/api/plane/api/serializers/project.py index 9ddb78ee516..38cb25c3099 100644 --- a/apps/api/plane/api/serializers/project.py +++ b/apps/api/plane/api/serializers/project.py @@ -106,13 +106,13 @@ class Meta: ] def validate(self, data): - project_name = data.get("name", None) is not None - project_identifier = data.get("identifier", None) is not None + project_name = data.get("name", None) + project_identifier = data.get("identifier", None) - if project_name and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + if project_name is not None and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): raise serializers.ValidationError("Project name cannot contain special characters.") - if project_identifier and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): + if project_identifier is not None and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): raise serializers.ValidationError("Project identifier cannot contain special characters.") if data.get("project_lead", None) is not None: @@ -174,13 +174,13 @@ class Meta(ProjectCreateSerializer.Meta): read_only_fields = ProjectCreateSerializer.Meta.read_only_fields def update(self, instance, validated_data): - project_name = validated_data.get("name", None) is not None - project_identifier = validated_data.get("identifier", None) is not None + project_name = validated_data.get("name", None) + project_identifier = validated_data.get("identifier", None) - if project_name and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + if project_name is not None and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): raise serializers.ValidationError("Project name cannot contain special characters.") - if project_identifier and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): + if project_identifier is not None and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): raise serializers.ValidationError("Project identifier cannot contain special characters.") """Update a project""" @@ -233,13 +233,13 @@ class Meta: ] def validate(self, data): - project_name = data.get("name", None) is not None - project_identifier = data.get("identifier", None) is not None + project_name = data.get("name", None) + project_identifier = data.get("identifier", None) - if project_name and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + if project_name is not None and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_name): raise serializers.ValidationError("Project name cannot contain special characters.") - if project_identifier and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): + if project_identifier is not None and re.match(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): raise serializers.ValidationError("Project identifier cannot contain special characters.") # Check project lead should be a member of the workspace From db5045cbb0efb2beb6545c7dd93e32bd067797aa Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 12 Jan 2026 16:25:38 +0530 Subject: [PATCH 4/5] update serializer to send error code --- apps/api/plane/app/serializers/project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/app/serializers/project.py b/apps/api/plane/app/serializers/project.py index 8cb994b045a..05b6c876ce8 100644 --- a/apps/api/plane/app/serializers/project.py +++ b/apps/api/plane/app/serializers/project.py @@ -42,7 +42,7 @@ def validate_name(self, name): workspace_id = self.context["workspace_id"] if re.match(FORBIDDEN_NAME_CHARS_PATTERN, name): - raise serializers.ValidationError("Project name cannot contain special characters.") + raise serializers.ValidationError(detail="PROJECT_NAME_CANNOT_CONTAIN_SPECIAL_CHARACTERS") project = Project.objects.filter(name=name, workspace_id=workspace_id) @@ -61,7 +61,7 @@ def validate_identifier(self, identifier): workspace_id = self.context["workspace_id"] if re.match(FORBIDDEN_NAME_CHARS_PATTERN, identifier): - raise serializers.ValidationError("Project identifier cannot contain special characters.") + raise serializers.ValidationError(detail="PROJECT_IDENTIFIER_CANNOT_CONTAIN_SPECIAL_CHARACTERS") project = Project.objects.filter(identifier=identifier, workspace_id=workspace_id) From 26412278580a0bb249c4875a2cc8d7a650559005 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 13 Jan 2026 14:45:51 +0530 Subject: [PATCH 5/5] fix: move the regex expression to Project model --- apps/api/plane/api/serializers/project.py | 17 ++++++----------- apps/api/plane/app/serializers/project.py | 9 ++------- apps/api/plane/db/models/project.py | 2 ++ 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/apps/api/plane/api/serializers/project.py b/apps/api/plane/api/serializers/project.py index 38cb25c3099..2238d1f74ae 100644 --- a/apps/api/plane/api/serializers/project.py +++ b/apps/api/plane/api/serializers/project.py @@ -14,11 +14,6 @@ ) from .base import BaseSerializer -# Regex pattern to block the following special characters in names and identifiers: -# & + , : ; $ ^ } { * = ? @ # | ' < > . ( ) % ! - -FORBIDDEN_NAME_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!].*$" - class ProjectCreateSerializer(BaseSerializer): """ @@ -109,10 +104,10 @@ 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(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + 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(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): + 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: @@ -177,10 +172,10 @@ 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(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + 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(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): + 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""" @@ -236,10 +231,10 @@ 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(FORBIDDEN_NAME_CHARS_PATTERN, project_name): + 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(FORBIDDEN_NAME_CHARS_PATTERN, project_identifier): + 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 diff --git a/apps/api/plane/app/serializers/project.py b/apps/api/plane/app/serializers/project.py index 05b6c876ce8..87f053a2bee 100644 --- a/apps/api/plane/app/serializers/project.py +++ b/apps/api/plane/app/serializers/project.py @@ -22,11 +22,6 @@ validate_html_content, ) -# Regex pattern to block the following special characters in names and identifiers: -# & + , : ; $ ^ } { * = ? @ # | ' < > . ( ) % ! - -FORBIDDEN_NAME_CHARS_PATTERN = r"^.*[&+,:;$^}{*=?@#|'<>.()%!].*$" - class ProjectSerializer(BaseSerializer): workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) @@ -41,7 +36,7 @@ def validate_name(self, name): project_id = self.instance.id if self.instance else None workspace_id = self.context["workspace_id"] - if re.match(FORBIDDEN_NAME_CHARS_PATTERN, name): + 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) @@ -60,7 +55,7 @@ def validate_identifier(self, identifier): project_id = self.instance.id if self.instance else None workspace_id = self.context["workspace_id"] - if re.match(FORBIDDEN_NAME_CHARS_PATTERN, identifier): + 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) diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 16281025bb2..7448286acbf 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -136,6 +136,8 @@ def __str__(self): """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"],