Skip to content

Commit ca38eaa

Browse files
committed
🐛(backend) enforce emoji validation for reactions
Validate emojis in ReactionSerializer (previously accepted any string), preventing multiple emojis or text uploads in a single reaction Signed-off-by: Mohamed El Amine BOUKERFA <boukerfa.ma@gmail.com>
1 parent b708c8b commit ca38eaa

5 files changed

Lines changed: 89 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to
2525

2626
### Fixed
2727

28+
- 🐛(backend) enforce emoji validation for reactions #1965
2829
- 🐛(frontend) analytic feature flags problem #1953
2930
- 🐛(frontend) fix home collapsing panel #1954
3031
- 🐛(frontend) fix disabled color on icon Dropdown #1950

src/backend/core/api/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import binascii
55
import mimetypes
66
from base64 import b64decode
7+
from logging import getLogger
78
from os.path import splitext
89

910
from django.conf import settings
@@ -12,6 +13,7 @@
1213
from django.utils.text import slugify
1314
from django.utils.translation import gettext_lazy as _
1415

16+
import emoji
1517
import magic
1618
from rest_framework import serializers
1719

@@ -23,6 +25,8 @@
2325
Converter,
2426
)
2527

28+
logger = getLogger(__name__)
29+
2630

2731
class UserSerializer(serializers.ModelSerializer):
2832
"""Serialize users."""
@@ -895,6 +899,17 @@ class Meta:
895899
]
896900
read_only_fields = ["id", "created_at", "users"]
897901

902+
def validate_emoji(self, value):
903+
"""Ensure the reaction is a single emoji."""
904+
if not emoji.is_emoji(value):
905+
logger.warning(
906+
"Invalid emoji rejected: '%s' (unicode=%s)",
907+
value,
908+
" ".join(f"U+{ord(c):04X}" for c in value) if value else "N/A",
909+
)
910+
raise serializers.ValidationError("Reaction must be a single valid emoji.")
911+
return value
912+
898913

899914
class CommentSerializer(serializers.ModelSerializer):
900915
"""Serialize comments (nested under a thread) with reactions and abilities."""

src/backend/core/factories.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,10 @@ class ReactionFactory(factory.django.DjangoModelFactory):
231231

232232
class Meta:
233233
model = models.Reaction
234+
skip_postgeneration_save = True
234235

235236
comment = factory.SubFactory(CommentFactory)
236-
emoji = "test"
237+
emoji = factory.Faker("emoji")
237238

238239
@factory.post_generation
239240
def users(self, create, extracted, **kwargs):

src/backend/core/tests/documents/test_api_documents_comments.py

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -644,11 +644,13 @@ def test_create_reaction_anonymous_user_public_document(link_role):
644644
document = factories.DocumentFactory(link_reach="public", link_role=link_role)
645645
thread = factories.ThreadFactory(document=document)
646646
comment = factories.CommentFactory(thread=thread)
647+
reaction = factories.ReactionFactory(comment=comment)
648+
647649
client = APIClient()
648650
response = client.post(
649651
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
650652
f"comments/{comment.id!s}/reactions/",
651-
{"emoji": "test"},
653+
{"emoji": reaction.emoji},
652654
)
653655
assert response.status_code == 401
654656

@@ -664,12 +666,14 @@ def test_create_reaction_authenticated_user_public_document():
664666
)
665667
thread = factories.ThreadFactory(document=document)
666668
comment = factories.CommentFactory(thread=thread)
669+
reaction = factories.ReactionFactory(comment=comment)
670+
667671
client = APIClient()
668672
client.force_login(user)
669673
response = client.post(
670674
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
671675
f"comments/{comment.id!s}/reactions/",
672-
{"emoji": "test"},
676+
{"emoji": reaction.emoji},
673677
)
674678
assert response.status_code == 403
675679

@@ -684,17 +688,19 @@ def test_create_reaction_authenticated_user_accessible_public_document():
684688
)
685689
thread = factories.ThreadFactory(document=document)
686690
comment = factories.CommentFactory(thread=thread)
691+
reaction = factories.ReactionFactory(comment=comment)
692+
687693
client = APIClient()
688694
client.force_login(user)
689695
response = client.post(
690696
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
691697
f"comments/{comment.id!s}/reactions/",
692-
{"emoji": "test"},
698+
{"emoji": reaction.emoji},
693699
)
694700
assert response.status_code == 201
695701

696702
assert models.Reaction.objects.filter(
697-
comment=comment, emoji="test", users__in=[user]
703+
comment=comment, emoji=reaction.emoji, users__in=[user]
698704
).exists()
699705

700706

@@ -709,12 +715,14 @@ def test_create_reaction_authenticated_user_connected_document_link_role_reader(
709715
)
710716
thread = factories.ThreadFactory(document=document)
711717
comment = factories.CommentFactory(thread=thread)
718+
reaction = factories.ReactionFactory(comment=comment)
719+
712720
client = APIClient()
713721
client.force_login(user)
714722
response = client.post(
715723
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
716724
f"comments/{comment.id!s}/reactions/",
717-
{"emoji": "test"},
725+
{"emoji": reaction.emoji},
718726
)
719727
assert response.status_code == 403
720728

@@ -737,17 +745,19 @@ def test_create_reaction_authenticated_user_connected_document(link_role):
737745
)
738746
thread = factories.ThreadFactory(document=document)
739747
comment = factories.CommentFactory(thread=thread)
748+
reaction = factories.ReactionFactory(comment=comment)
749+
740750
client = APIClient()
741751
client.force_login(user)
742752
response = client.post(
743753
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
744754
f"comments/{comment.id!s}/reactions/",
745-
{"emoji": "test"},
755+
{"emoji": reaction.emoji},
746756
)
747757
assert response.status_code == 201
748758

749759
assert models.Reaction.objects.filter(
750-
comment=comment, emoji="test", users__in=[user]
760+
comment=comment, emoji=reaction.emoji, users__in=[user]
751761
).exists()
752762

753763

@@ -760,12 +770,14 @@ def test_create_reaction_authenticated_user_restricted_accessible_document():
760770
document = factories.DocumentFactory(link_reach="restricted")
761771
thread = factories.ThreadFactory(document=document)
762772
comment = factories.CommentFactory(thread=thread)
773+
reaction = factories.ReactionFactory(comment=comment)
774+
763775
client = APIClient()
764776
client.force_login(user)
765777
response = client.post(
766778
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
767779
f"comments/{comment.id!s}/reactions/",
768-
{"emoji": "test"},
780+
{"emoji": reaction.emoji},
769781
)
770782
assert response.status_code == 403
771783

@@ -781,12 +793,14 @@ def test_create_reaction_authenticated_user_restricted_accessible_document_role_
781793
)
782794
thread = factories.ThreadFactory(document=document)
783795
comment = factories.CommentFactory(thread=thread)
796+
reaction = factories.ReactionFactory(comment=comment)
797+
784798
client = APIClient()
785799
client.force_login(user)
786800
response = client.post(
787801
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
788802
f"comments/{comment.id!s}/reactions/",
789-
{"emoji": "test"},
803+
{"emoji": reaction.emoji},
790804
)
791805
assert response.status_code == 403
792806

@@ -806,28 +820,72 @@ def test_create_reaction_authenticated_user_restricted_accessible_document_role_
806820
document = factories.DocumentFactory(link_reach="restricted", users=[(user, role)])
807821
thread = factories.ThreadFactory(document=document)
808822
comment = factories.CommentFactory(thread=thread)
823+
reaction = factories.ReactionFactory(comment=comment)
824+
809825
client = APIClient()
810826
client.force_login(user)
811827
response = client.post(
812828
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
813829
f"comments/{comment.id!s}/reactions/",
814-
{"emoji": "test"},
830+
{"emoji": reaction.emoji},
815831
)
816832
assert response.status_code == 201
817833

818834
assert models.Reaction.objects.filter(
819-
comment=comment, emoji="test", users__in=[user]
835+
comment=comment, emoji=reaction.emoji, users__in=[user]
820836
).exists()
821837

822838
response = client.post(
823839
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
824840
f"comments/{comment.id!s}/reactions/",
825-
{"emoji": "test"},
841+
{"emoji": reaction.emoji},
826842
)
827843
assert response.status_code == 400
828844
assert response.json() == {"user_already_reacted": True}
829845

830846

847+
def test_create_reaction_invalid_emoji():
848+
"""Users should not be able to submit non-emojis as reactions."""
849+
user = factories.UserFactory()
850+
document = factories.DocumentFactory(
851+
link_reach="restricted", users=[(user, models.RoleChoices.COMMENTER)]
852+
)
853+
thread = factories.ThreadFactory(document=document)
854+
comment = factories.CommentFactory(thread=thread)
855+
856+
client = APIClient()
857+
client.force_login(user)
858+
859+
response = client.post(
860+
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
861+
f"comments/{comment.id!s}/reactions/",
862+
{"emoji": "test"},
863+
)
864+
assert response.status_code == 400
865+
assert "Reaction must be a single valid emoji." in str(response.json())
866+
867+
868+
def test_create_reaction_multiple_emojis():
869+
"""Users should not be able to submit multiple emojis as a single reaction."""
870+
user = factories.UserFactory()
871+
document = factories.DocumentFactory(
872+
link_reach="restricted", users=[(user, models.RoleChoices.COMMENTER)]
873+
)
874+
thread = factories.ThreadFactory(document=document)
875+
comment = factories.CommentFactory(thread=thread)
876+
877+
client = APIClient()
878+
client.force_login(user)
879+
880+
response = client.post(
881+
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
882+
f"comments/{comment.id!s}/reactions/",
883+
{"emoji": "🐛🐛"},
884+
)
885+
assert response.status_code == 400
886+
assert "Reaction must be a single valid emoji." in str(response.json())
887+
888+
831889
# Delete reaction
832890

833891

src/backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies = [
4545
"drf_spectacular==0.29.0",
4646
"dockerflow==2026.1.26",
4747
"easy_thumbnails==2.10.1",
48+
"emoji==2.15.0",
4849
"factory_boy==3.3.3",
4950
"gunicorn==25.1.0",
5051
"jsonschema==4.26.0",

0 commit comments

Comments
 (0)