Skip to content

Commit e49a57d

Browse files
committed
Also sanitize edge identifiers
1 parent 8f8167f commit e49a57d

File tree

5 files changed

+97
-9
lines changed

5 files changed

+97
-9
lines changed

api/edge_api/identities/serializers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from rest_framework.exceptions import ValidationError
1919

2020
from environments.dynamodb.types import IdentityOverrideV2
21+
from environments.identities.constants import identifier_regex_validator
2122
from environments.models import Environment
2223
from features.models import Feature, FeatureState, FeatureStateValue
2324
from features.multivariate.models import MultivariateFeatureOption
@@ -52,7 +53,11 @@ def to_internal_value(self, data: typing.Any) -> str:
5253

5354
class EdgeIdentitySerializer(serializers.Serializer): # type: ignore[type-arg]
5455
identity_uuid = serializers.CharField(read_only=True)
55-
identifier = serializers.CharField(required=True, max_length=2000)
56+
identifier = serializers.CharField(
57+
required=True,
58+
max_length=2000,
59+
validators=[identifier_regex_validator],
60+
)
5661
dashboard_alias = LowerCaseCharField(
5762
required=False,
5863
max_length=100,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import re
2+
3+
from django.core.validators import RegexValidator
4+
5+
RE_VALID_IDENTIFIER = re.compile(r"^[\w!#$%&*+/=?^_`{}|~@.\-]+$")
6+
7+
identifier_regex_validator = RegexValidator(
8+
regex=RE_VALID_IDENTIFIER,
9+
message="Identifier can only contain unicode letters, numbers, and the symbols: ! # $ %% & * + / = ? ^ _ ` { } | ~ @ . -",
10+
)

api/environments/identities/migrations/0003_sanitized_identifiers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Generated by Django 4.2.22 on 2025-09-08 23:30
22

3+
import re
4+
35
import django.core.validators
46
from django.db import migrations, models
57

@@ -19,7 +21,7 @@ class Migration(migrations.Migration):
1921
validators=[
2022
django.core.validators.RegexValidator(
2123
message="Identifier can only contain unicode letters, numbers, and the symbols: ! # $ %% & * + / = ? ^ _ ` { } | ~ @ . -",
22-
regex="^[\\w!#$%&*+/=?^_`{}|~@.\\-]+$",
24+
regex=re.compile("^[\\w!#$%&*+/=?^_`{}|~@.\\-]+$"),
2325
)
2426
],
2527
),

api/environments/identities/models.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from itertools import chain
22

3-
from django.core.validators import RegexValidator
43
from django.db import models
54
from django.db.models import Prefetch, Q
65
from flag_engine.context.mappers import map_environment_identity_to_context
76
from flag_engine.segments.evaluator import is_context_in_segment
87

8+
from environments.identities.constants import identifier_regex_validator
99
from environments.identities.managers import IdentityManager
1010
from environments.identities.traits.models import Trait
1111
from environments.models import Environment
@@ -24,12 +24,7 @@
2424
class Identity(models.Model):
2525
identifier = models.CharField(
2626
max_length=2000,
27-
validators=[
28-
RegexValidator(
29-
regex=r"^[\w!#$%&*+/=?^_`{}|~@.\-]+$",
30-
message="Identifier can only contain unicode letters, numbers, and the symbols: ! # $ %% & * + / = ? ^ _ ` { } | ~ @ . -",
31-
),
32-
],
27+
validators=[identifier_regex_validator],
3328
)
3429
created_date = models.DateTimeField("DateCreated", auto_now_add=True)
3530
environment = models.ForeignKey(

api/tests/integration/edge_api/identities/test_edge_identity_viewset.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import json
22
import urllib
33
from typing import Any
4+
from unittest.mock import Mock
45

6+
import pytest
57
from boto3.dynamodb.conditions import Key
68
from django.urls import reverse
79
from mypy_boto3_dynamodb.service_resource import Table
@@ -18,6 +20,8 @@
1820
)
1921
from environments.models import Environment
2022

23+
_invalid_identifier_error_message = "Identifier can only contain unicode letters, numbers, and the symbols: ! # $ % & * + / = ? ^ _ ` { } | ~ @ . -"
24+
2125

2226
def test_get_identities_returns_bad_request_if_dynamo_is_not_enabled( # type: ignore[no-untyped-def]
2327
admin_client, environment, environment_api_key
@@ -116,6 +120,78 @@ def test_create_identity( # type: ignore[no-untyped-def]
116120
assert response.json()["identity_uuid"] is not None
117121

118122

123+
@pytest.mark.parametrize(
124+
"given_identifier",
125+
[
126+
"bond...jamesbond",
127+
"ゴジラ",
128+
"ElChapulínColorado",
129+
130+
"agáta={^_^}=",
131+
"_ツ_/-handless-shrug",
132+
"who+am+i?",
133+
"i_100%_dont_know!",
134+
"~neo|simulation`0065192*75`",
135+
"KacperGustyr$Flagsmat",
136+
],
137+
)
138+
@pytest.mark.usefixtures(
139+
"dynamo_enabled_environment",
140+
)
141+
def test_create_identity_accepts_valid_identifiers(
142+
admin_client: APIClient,
143+
environment_api_key: str,
144+
given_identifier: str,
145+
edge_identity_dynamo_wrapper_mock: Mock,
146+
) -> None:
147+
# Given
148+
edge_identity_dynamo_wrapper_mock.get_item.return_value = None
149+
150+
# When
151+
response = admin_client.post(
152+
f"/api/v1/environments/{environment_api_key}/edge-identities/",
153+
data={"identifier": given_identifier},
154+
)
155+
156+
# Then
157+
assert response.status_code == status.HTTP_201_CREATED
158+
assert response.json()["identifier"] == given_identifier
159+
160+
161+
@pytest.mark.parametrize(
162+
["given_identifier", "error_message"],
163+
[
164+
("", "This field may not be blank."),
165+
(" ", "This field may not be blank."),
166+
("or really anything with a whitespace", _invalid_identifier_error_message),
167+
("<script>alert(1)</script>", _invalid_identifier_error_message),
168+
("'; DROP TABLE users;--", _invalid_identifier_error_message),
169+
("'single-quotes'", _invalid_identifier_error_message),
170+
('"double-quotes"', _invalid_identifier_error_message),
171+
("figaro" * 334, "Ensure this field has no more than 2000 characters."),
172+
],
173+
)
174+
@pytest.mark.usefixtures(
175+
"dynamo_enabled_environment",
176+
"edge_identity_dynamo_wrapper_mock",
177+
)
178+
def test_create_identity_responds_400_if_identifier_is_invalid(
179+
admin_client: APIClient,
180+
environment_api_key: str,
181+
error_message: str,
182+
given_identifier: str,
183+
) -> None:
184+
# When
185+
response = admin_client.post(
186+
f"/api/v1/environments/{environment_api_key}/edge-identities/",
187+
data={"identifier": given_identifier},
188+
)
189+
190+
# Then
191+
assert response.status_code == status.HTTP_400_BAD_REQUEST
192+
assert response.json() == {"identifier": [error_message]}
193+
194+
119195
def test_create_identity_returns_400_if_identity_already_exists( # type: ignore[no-untyped-def]
120196
admin_client,
121197
dynamo_enabled_environment,

0 commit comments

Comments
 (0)