From ab6a674744fd31fa99d8e2eee8b59bcb4788e650 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 14 Nov 2024 14:15:06 +0100 Subject: [PATCH 1/2] security: fix vuln where users could delete relationships that didnt belong to them --- backend/api/views/users.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/api/views/users.py b/backend/api/views/users.py index f64df77f..69a20917 100644 --- a/backend/api/views/users.py +++ b/backend/api/views/users.py @@ -290,15 +290,7 @@ def put(self, request, *args, **kwargs): return Response({"error": "Invalid data"}, status=status.HTTP_400_BAD_REQUEST) if target_user_id == "user_ai": - if relationship_type == 2: # tries to block prune - return Response({"error": "There's no escaping prune, nice try."}, status=status.HTTP_400_BAD_REQUEST) - relationship = Relationship.objects.create( - relationshipID=generate_id("rel"), - userA=me.userID, - userB=target_user_id, - status=1 # Accepted/friends status - ) - return Response({"status": "AI friend added, you must feel very lonely. don't worry Prune is a good friend."}, status=status.HTTP_200_OK) + return Response({"error": "There's no escaping prune, nice try."}, status=status.HTTP_400_BAD_REQUEST) try: target_user = User.objects.get(userID=target_user_id) @@ -357,6 +349,13 @@ def delete(self, request, relationshipID, *args, **kwargs): relationshipID=relationshipID ) + # Check if the user is part of the relationship + if me.userID != relationship.userA and me.userID != relationship.userB: + return Response({"error": "You are not part of this relationship"}, status=status.HTTP_403_FORBIDDEN) + + if relationship.userA == "user_ai" or relationship.userB == "user_ai": + return Response({"error": "There's no escaping prune, nice try."}, status=status.HTTP_400_BAD_REQUEST) + if relationship.status == 0: if me.userID != relationship.userA: self.notify_chat_websocket(relationship, status="rejected") From 1d88240480e40c1c4bbfbfdf13cc2311e1cdc382 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 14 Nov 2024 14:15:19 +0100 Subject: [PATCH 2/2] prune: sending warning for tournament matches --- backend/api/apps.py | 11 +++++- backend/api/consumers/tournaments.py | 55 ++++++++++++++++++++++++++-- backend/api/views/auth.py | 10 ++++- backend/api/views/oauth.py | 9 ++++- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/backend/api/apps.py b/backend/api/apps.py index 9684de73..4b7531d0 100644 --- a/backend/api/apps.py +++ b/backend/api/apps.py @@ -90,13 +90,13 @@ def update_users_statuses(sender, **kwargs): User.objects.bulk_update(users, ['status']) def create_test_accounts(sender, **kwargs): - from .models import User + from .models import User, Relationship if os.environ.get('SKIP_EMAIL_VERIFICATION', '').lower() == 'true': for i in range(1, 5): username = f'test{i}' if not User.objects.filter(username=username).exists(): - User.objects.create( + user = User.objects.create( userID=generate_id('user'), username=username, email=f'{username}@example.com', @@ -105,3 +105,10 @@ def create_test_accounts(sender, **kwargs): flags=1, # Set EMAIL_VERIFIED flag money=100000 ) + + Relationship.objects.create( + relationshipID=generate_id("rel"), + userA=user.userID, + userB="user_ai", + status=1 # Accepted/friends status + ) diff --git a/backend/api/consumers/tournaments.py b/backend/api/consumers/tournaments.py index e4c66268..2e2962db 100644 --- a/backend/api/consumers/tournaments.py +++ b/backend/api/consumers/tournaments.py @@ -9,11 +9,11 @@ from django.conf import settings from django.core.cache import cache from django.utils import timezone -from django.db.models import Q +from django.db.models import Q, Count -from ..models import User, Tournament, Match +from ..models import User, Tournament, Match, Conversation from ..util import get_safe_profile, generate_id -from ..serializers import UserSerializer, MatchSerializer +from ..serializers import UserSerializer, MatchSerializer, MessageSerializer from asgiref.sync import sync_to_async @@ -212,10 +212,59 @@ async def start_next_match(self): } ) + # Warn players of the next match + for player in next_match['players']: + await self.send_prune_message(player['userID'], f"Your next match in tournament '{self.tournament.name}' is starting soon!") + + # Get the next next match + next_next_match = await self.get_next_unstarted_match() + if next_next_match: + for player in next_next_match['players']: + await self.send_prune_message(player['userID'], f"Your match in tournament '{self.tournament.name}' is coming up next. Please be ready!") + return next_match finally: await self.delete_lock(lock_key) + async def send_prune_message(self, user_id, content): + prune_user = await sync_to_async(User.objects.get)(userID="user_ai") + conversation = await self.get_or_create_prune_conversation(user_id) + + message = await sync_to_async(conversation.messages.create)( + messageID=generate_id("msg"), + sender=prune_user, + content=content + ) + + await self.channel_layer.group_send( + f"chat_{user_id}", + { + "type": "conversation_update", + "conversationID": conversation.conversationID, + "sender": get_safe_profile(UserSerializer(prune_user).data, me=False), + "message": MessageSerializer(message).data + } + ) + @sync_to_async + def get_or_create_prune_conversation(self, user_id): + user = User.objects.get(userID=user_id) + prune_user = User.objects.get(userID="user_ai") + + conversation = Conversation.objects.filter( + participants__userID__in=[user_id, "user_ai"], + conversationType='private_message' + ).annotate(participant_count=Count('participants')).filter(participant_count=2).first() + + if not conversation: + conversation = Conversation.objects.create( + conversationID=generate_id("conv"), + conversationType='private_message' + ) + conversation.participants.add(user, prune_user) + conversation.save() + + return conversation + @sync_to_async def set_match_start_time(self, match_id): Match.objects.filter(matchID=match_id, startedAt__isnull=True).update(startedAt=timezone.now()) diff --git a/backend/api/views/auth.py b/backend/api/views/auth.py index 27cce183..0959ab97 100644 --- a/backend/api/views/auth.py +++ b/backend/api/views/auth.py @@ -16,7 +16,7 @@ from django.utils import timezone from django.contrib.auth.hashers import check_password -from ..models import User, VerificationCode +from ..models import User, VerificationCode, Relationship from ..serializers import UserSerializer from ..util import generate_id, send_verification_email, send_otp_via_email, send_otp_via_sms from ..backends import AuthBackend @@ -52,6 +52,14 @@ def post(self, request, *args, **kwargs): flags=1 if skip_email_verification else 0 # Set EMAIL_VERIFIED flag if skipping verification ) + # Create a relationship with the AI user + Relationship.objects.create( + relationshipID=generate_id("rel"), + userA=user.userID, + userB="user_ai", + status=1 # Accepted/friends status + ) + if not skip_email_verification: verification_code = generate_id('code') VerificationCode.objects.create( diff --git a/backend/api/views/oauth.py b/backend/api/views/oauth.py index 7f1ac0b8..e476c010 100644 --- a/backend/api/views/oauth.py +++ b/backend/api/views/oauth.py @@ -15,7 +15,7 @@ from rest_framework_simplejwt.tokens import RefreshToken from ..util import generate_id, send_welcome_email -from ..models import User +from ..models import User, Relationship @permission_classes([AllowAny]) class OAuth42Login(APIView): @@ -82,6 +82,13 @@ def get(self, request): if created: send_welcome_email(user.email) + # Create a relationship with the AI user + Relationship.objects.create( + relationshipID=generate_id("rel"), + userA=user.userID, + userB="user_ai", + status=1 # Accepted/friends status + ) login(request, user)