From 5b9d15c0f1d803dcc81bca9d6e33ba294111e2d4 Mon Sep 17 00:00:00 2001 From: sriramveeraghanta Date: Sat, 20 Jun 2026 17:13:19 +0530 Subject: [PATCH 1/2] fix(api): require at least one alphanumeric char in workspace name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace name validation was enforced only on the frontend (validateWorkspaceName), which gates the UI submit but is bypassable via a direct API call. The backend WorkSpaceSerializer.validate_name only rejected URLs, so a symbol-only name like "-_________-" could still be saved via create or the rename (partial_update) path. Add a Unicode-aware has_alphanumeric() helper and enforce it in both the app and instance/license workspace serializers, mirroring the frontend HAS_ALPHANUMERIC_REGEX (/[\p{L}\p{N}]/u) added in #9263. International names (日本語, José, محمد) still pass since str.isalnum() covers all scripts. Adds unit tests covering symbol-only rejection and international acceptance on both serializers. Refs #9255 Signed-off-by: sriramveeraghanta --- apps/api/plane/app/serializers/workspace.py | 8 +++ .../license/api/serializers/workspace.py | 11 ++++ .../tests/unit/serializers/test_workspace.py | 59 +++++++++++++++++++ apps/api/plane/utils/content_validator.py | 18 ++++++ 4 files changed, 96 insertions(+) diff --git a/apps/api/plane/app/serializers/workspace.py b/apps/api/plane/app/serializers/workspace.py index 608cdad8517..940b2c58d5a 100644 --- a/apps/api/plane/app/serializers/workspace.py +++ b/apps/api/plane/app/serializers/workspace.py @@ -31,6 +31,7 @@ from plane.utils.content_validator import ( validate_html_content, validate_binary_data, + has_alphanumeric, ) # Django imports @@ -48,6 +49,13 @@ def validate_name(self, value): # Check if the name contains a URL if contains_url(value): raise serializers.ValidationError("Name must not contain URLs") + # Reject symbol-only names like "-_________-" that have no letter or + # digit. Mirrors the frontend HAS_ALPHANUMERIC_REGEX check so the rule + # cannot be bypassed via a direct API call. + if not has_alphanumeric(value): + raise serializers.ValidationError( + "Name must contain at least one letter or number" + ) return value def validate_slug(self, value): diff --git a/apps/api/plane/license/api/serializers/workspace.py b/apps/api/plane/license/api/serializers/workspace.py index d12473e2047..160d0f4fa4c 100644 --- a/apps/api/plane/license/api/serializers/workspace.py +++ b/apps/api/plane/license/api/serializers/workspace.py @@ -10,6 +10,7 @@ from .user import UserLiteSerializer from plane.db.models import Workspace from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS +from plane.utils.content_validator import has_alphanumeric class WorkspaceSerializer(BaseSerializer): @@ -18,6 +19,16 @@ class WorkspaceSerializer(BaseSerializer): total_projects = serializers.IntegerField(read_only=True) total_members = serializers.IntegerField(read_only=True) + def validate_name(self, value): + # Reject symbol-only names like "-_________-" that have no letter or + # digit. Mirrors the frontend HAS_ALPHANUMERIC_REGEX check so the rule + # cannot be bypassed via a direct API call. + if not has_alphanumeric(value): + raise serializers.ValidationError( + "Name must contain at least one letter or number" + ) + return value + def validate_slug(self, value): # Check if the slug is restricted if value in RESTRICTED_WORKSPACE_SLUGS: diff --git a/apps/api/plane/tests/unit/serializers/test_workspace.py b/apps/api/plane/tests/unit/serializers/test_workspace.py index f59667f701b..20cce09c80f 100644 --- a/apps/api/plane/tests/unit/serializers/test_workspace.py +++ b/apps/api/plane/tests/unit/serializers/test_workspace.py @@ -5,9 +5,31 @@ import pytest from uuid import uuid4 +from rest_framework import serializers + from plane.api.serializers import WorkspaceLiteSerializer +from plane.app.serializers import WorkSpaceSerializer +from plane.license.api.serializers import WorkspaceSerializer as InstanceWorkspaceSerializer from plane.db.models import Workspace, User +# Names with no letter or digit — must be rejected (issue #9255) +SYMBOL_ONLY_NAMES = ["-_________-", "---", "___", "- - -", " ", " -_ "] + +# Names containing at least one letter or digit — must be accepted, including +# international scripts, so the rule does not regress non-Latin workspace names +VALID_NAMES = [ + "Acme Corp", + "Acme_Corp-123", + "123", + "a", + "R&D", + "José", + "Müller GmbH", + "日本語", + "株式会社", + "محمد", +] + @pytest.mark.unit class TestWorkspaceLiteSerializer: @@ -52,3 +74,40 @@ def test_workspace_lite_serializer_read_only(self, db): updated_workspace = serializer.save() assert updated_workspace.name == "Test Workspace" assert updated_workspace.slug == "test-workspace" + + +@pytest.mark.unit +class TestWorkSpaceSerializerNameValidation: + """validate_name must reject symbol-only workspace names (issue #9255). + + Frontend validation is bypassable via a direct API call, so the rule is + enforced server-side on both the create and rename (partial_update) paths, + which share this serializer's field-level validation. + """ + + @pytest.mark.parametrize("name", SYMBOL_ONLY_NAMES) + def test_rejects_symbol_only_names(self, name): + serializer = WorkSpaceSerializer() + with pytest.raises(serializers.ValidationError): + serializer.validate_name(name) + + @pytest.mark.parametrize("name", VALID_NAMES) + def test_accepts_names_with_alphanumeric(self, name): + serializer = WorkSpaceSerializer() + assert serializer.validate_name(name) == name + + +@pytest.mark.unit +class TestInstanceWorkspaceSerializerNameValidation: + """The instance/license workspace create path must enforce the same rule.""" + + @pytest.mark.parametrize("name", SYMBOL_ONLY_NAMES) + def test_rejects_symbol_only_names(self, name): + serializer = InstanceWorkspaceSerializer() + with pytest.raises(serializers.ValidationError): + serializer.validate_name(name) + + @pytest.mark.parametrize("name", VALID_NAMES) + def test_accepts_names_with_alphanumeric(self, name): + serializer = InstanceWorkspaceSerializer() + assert serializer.validate_name(name) == name diff --git a/apps/api/plane/utils/content_validator.py b/apps/api/plane/utils/content_validator.py index 1b4ede2626f..711d560fac0 100644 --- a/apps/api/plane/utils/content_validator.py +++ b/apps/api/plane/utils/content_validator.py @@ -241,3 +241,21 @@ def validate_html_content(html_content: str): except Exception as e: log_exception(e) return False, "Failed to sanitize HTML", None + + +def has_alphanumeric(value): + """ + Check whether a string contains at least one alphanumeric character. + + `str.isalnum()` is Unicode-aware, so letters and digits from any script + (Latin, CJK, Arabic, Cyrillic, etc.) all count. This mirrors the frontend + HAS_ALPHANUMERIC_REGEX (/[\\p{L}\\p{N}]/u) check and is used to reject + symbol-only names such as "-_________-". + + Args: + value (str): The string to check. + + Returns: + bool: True if the value contains at least one letter or digit. + """ + return any(char.isalnum() for char in (value or "")) From 1dfa3cde88623cbf5f0978eaeab0ef294e526437 Mon Sep 17 00:00:00 2001 From: sriramveeraghanta Date: Sat, 20 Jun 2026 17:31:22 +0530 Subject: [PATCH 2/2] fix(api): reject URLs in instance workspace name for parity Address CodeRabbit review on #9278: the instance/license WorkspaceSerializer.validate_name rejected symbol-only names but, unlike the app-level WorkSpaceSerializer, still accepted names containing URLs. Add the same contains_url() guard (imported from plane.utils.url, not content_validator) so both workspace-create paths validate identically. Add unit tests asserting URL-containing names are rejected on both serializers. Signed-off-by: sriramveeraghanta --- .../plane/license/api/serializers/workspace.py | 5 +++++ .../tests/unit/serializers/test_workspace.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/license/api/serializers/workspace.py b/apps/api/plane/license/api/serializers/workspace.py index 160d0f4fa4c..dd10766fdbc 100644 --- a/apps/api/plane/license/api/serializers/workspace.py +++ b/apps/api/plane/license/api/serializers/workspace.py @@ -11,6 +11,7 @@ from plane.db.models import Workspace from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS from plane.utils.content_validator import has_alphanumeric +from plane.utils.url import contains_url class WorkspaceSerializer(BaseSerializer): @@ -20,6 +21,10 @@ class WorkspaceSerializer(BaseSerializer): total_members = serializers.IntegerField(read_only=True) def validate_name(self, value): + # Check if the name contains a URL (kept consistent with the app-level + # WorkSpaceSerializer so both workspace-create paths validate alike). + if contains_url(value): + raise serializers.ValidationError("Name must not contain URLs") # Reject symbol-only names like "-_________-" that have no letter or # digit. Mirrors the frontend HAS_ALPHANUMERIC_REGEX check so the rule # cannot be bypassed via a direct API call. diff --git a/apps/api/plane/tests/unit/serializers/test_workspace.py b/apps/api/plane/tests/unit/serializers/test_workspace.py index 20cce09c80f..554deba6246 100644 --- a/apps/api/plane/tests/unit/serializers/test_workspace.py +++ b/apps/api/plane/tests/unit/serializers/test_workspace.py @@ -30,6 +30,9 @@ "محمد", ] +# Names embedding a URL — must be rejected on both create paths +URL_NAMES = ["https://evil.com", "www.example.com", "example.com"] + @pytest.mark.unit class TestWorkspaceLiteSerializer: @@ -96,10 +99,17 @@ def test_accepts_names_with_alphanumeric(self, name): serializer = WorkSpaceSerializer() assert serializer.validate_name(name) == name + @pytest.mark.parametrize("name", URL_NAMES) + def test_rejects_names_containing_urls(self, name): + serializer = WorkSpaceSerializer() + with pytest.raises(serializers.ValidationError): + serializer.validate_name(name) + @pytest.mark.unit class TestInstanceWorkspaceSerializerNameValidation: - """The instance/license workspace create path must enforce the same rule.""" + """The instance/license workspace create path must enforce the same rules + as the app serializer (symbol-only rejection AND URL rejection).""" @pytest.mark.parametrize("name", SYMBOL_ONLY_NAMES) def test_rejects_symbol_only_names(self, name): @@ -111,3 +121,9 @@ def test_rejects_symbol_only_names(self, name): def test_accepts_names_with_alphanumeric(self, name): serializer = InstanceWorkspaceSerializer() assert serializer.validate_name(name) == name + + @pytest.mark.parametrize("name", URL_NAMES) + def test_rejects_names_containing_urls(self, name): + serializer = InstanceWorkspaceSerializer() + with pytest.raises(serializers.ValidationError): + serializer.validate_name(name)