Skip to content

Commit 30f07eb

Browse files
authored
fix: Reject passwords with emoji (#2042)
🔗 [Jira Ticket M2-10505](https://mindlogger.atlassian.net/browse/M2-10505) Changes include: - Reject passwords that contain standard emoji or regional indicators. This is a follow-up to pull requests #2036 and #2038 that brings backend password validation in line with what @adeiji implemented for emojis on the front end in: - ChildMindInstitute/mindlogger-admin#2207 - ChildMindInstitute/mindlogger-app-refactor#1089 - ChildMindInstitute/mindlogger-web-refactor#719
1 parent 924e312 commit 30f07eb

3 files changed

Lines changed: 35 additions & 4 deletions

File tree

src/apps/users/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class PasswordHasSpacesError(ValidationError):
2020
message = _("Password must not contain spaces.")
2121

2222

23+
class PasswordHasEmojisError(ValidationError):
24+
message = _("Password must not contain emojis.")
25+
26+
2327
class PasswordContainsInvalidCharactersError(ValidationError):
2428
message = _("Password must not contain control characters.")
2529

src/apps/users/password_validation.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@
44

55
from apps.users.errors import (
66
PasswordContainsInvalidCharactersError,
7+
PasswordHasEmojisError,
78
PasswordHasSpacesError,
89
PasswordInsufficientTypesError,
910
PasswordTooShortError,
1011
)
1112
from config import settings
1213

14+
# Regex for emoji (standard emjoi + regional indicators) which are not exposed as a Unicode catgory
15+
EMOJI_REGEX = regex.compile(r"\p{Extended_Pictographic}|[\U0001F1E6-\U0001F1FF]")
16+
17+
# Regex for grapheme cluster for counting user-perceived characters
18+
GRAPHEME_REGEX = regex.compile(r"\X")
19+
1320

1421
class PasswordValidator:
1522
"""Single source of truth for all password validation logic."""
@@ -40,8 +47,14 @@ def validate(cls, password: str) -> str:
4047
if has_whitespace:
4148
raise PasswordHasSpacesError()
4249

43-
# Minimum length as counted by '\X' graphemes
44-
if len(regex.findall(r"\X", normalized)) < config.min_length:
50+
# Reject any emoji or regional indicators
51+
has_emoji = EMOJI_REGEX.search(normalized) is not None
52+
if has_emoji:
53+
raise PasswordHasEmojisError()
54+
55+
# Minimum length as counted by graphemes
56+
length = len(GRAPHEME_REGEX.findall(normalized))
57+
if length < config.min_length:
4558
raise PasswordTooShortError(chars=config.min_length)
4659

4760
# At least N of the following character types

src/apps/users/tests/unit/test_user_domain.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ def test_public_user_from_user_model(base_data: BaseData):
9696
"\u65e5123456789", # 3 types: lower (via caseless CJK) + upper (via caseless CJK) + digit
9797
"TestPass1!", # 4 types: lower + upper + digit + symbol
9898
"TestPass1!n\u0303", # 4 types: NFKC normalizes n + combining ~ to ñ
99-
"Abcdefgh!\U0001f1fa\U0001f1f3", # 9 chars + flag emoji (2 code points / 1 grapheme)
10099
],
101100
)
102101
def test_user_create_request_valid_passwords(
@@ -113,7 +112,6 @@ def test_user_create_request_valid_passwords(
113112
"Short1!aa", # 9 chars
114113
"weak", # 4 chars
115114
"", # 0 chars
116-
"Abcdefg!\U0001f1fa\U0001f1f3", # 8 chars + flag emoji (2 code points / 1 grapheme)
117115
],
118116
)
119117
def test_user_create_request_too_short_password_is_not_allowed(
@@ -165,6 +163,22 @@ def test_user_create_request_control_characters_are_not_allowed_in_password(
165163
domain.UserCreateRequest(**base_data)
166164

167165

166+
@pytest.mark.parametrize(
167+
"password",
168+
[
169+
"TestPass1!\U0001f600", # grinning face (standard emoji)
170+
"TestPass1!\U0001f1fa\U0001f1f3", # flag (regional indicator)
171+
],
172+
)
173+
def test_user_create_request_emojis_are_not_allowed_in_password(
174+
base_data: BaseData,
175+
password: str,
176+
):
177+
base_data["password"] = password
178+
with pytest.raises(errors.PasswordHasEmojisError):
179+
domain.UserCreateRequest(**base_data)
180+
181+
168182
def test_change_password_new_password_rejects_whitespace(
169183
base_data: BaseData,
170184
):

0 commit comments

Comments
 (0)