Security review: staff admin lockdown, unicode-safe login/phone/grade inputs, demo guest not upgradable, home cache not floodable
- accounts admin: phone/is_guest read-only for non-superusers, device_id filter lookups refused, auth.User/Group admin writable only by superusers - campaigns: delivery log needs view permission - phone/OTP/birth date: any Unicode digit normalised or rejected with 400; unknown gradeId is 400 - shared demo guest is never turned into a person's account by logging in - /api/home with unknown gradeId is no longer cached (LocMemCache flood reset throttle counters) - /api/progress/nodes: one query, huge unit is 400 - BazaarCredential token filter lookups refused Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .phone import normalize_phone
|
||||
from .phone import ascii_digits, normalize_phone
|
||||
|
||||
# لینک، آیدیِ شبکهی اجتماعی، و هر رشتهی ۵ رقمی یا بیشتر (شمارهی تلفن با
|
||||
# فاصله/خطتیره هم) — ارقامِ فارسی و عربی هم. این نام در رتبهبندی به بچههای دیگر نشان داده میشود.
|
||||
# لینک، آیدیِ شبکهی اجتماعی، و هر رشتهی ۵ رقمی یا بیشتر با هر جداکنندهی غیرِ حرفی (فاصله،
|
||||
# خطتیره، ستاره، ایموجی…). \d در پایتون هر رقمِ یونیکد است (فارسی، عربی، تمامعرض، ریاضی).
|
||||
# این نام در رتبهبندی به بچههای دیگر نشان داده میشود. حرف بینِ رقمها («0x9x1…») و عدد با
|
||||
# حروف («صفر نه یک…») گرفته نمیشود.
|
||||
_CONTACT = re.compile(
|
||||
r'(https?://\S+|www\.\S+|\S+\.(?:com|ir|org|net|me)\S*|@\w+'
|
||||
r'|(?:[0-9۰-۹٠-٩][\s\-._/\\|,،]*){5,})',
|
||||
r'|(?:\d(?:[^\w@]|_)*){5,})',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# «ig: name»، «insta name»، «آیدی name» و مانندش: کلیدواژه و هر چه بعدش آمده حذف میشود.
|
||||
@@ -20,7 +23,9 @@ _HANDLE = re.compile(
|
||||
|
||||
|
||||
def clean_display_name(value: str) -> str:
|
||||
value = _HANDLE.sub(' ', value or '')
|
||||
# NFKC: «①»، «²»، «𝟬» و «@» به رقم و @ِ معمولی برمیگردند تا الگوهای پایین آنها را هم بگیرند؛
|
||||
# نامِ ذخیرهشده همین شکلِ نرمال است.
|
||||
value = _HANDLE.sub(' ', unicodedata.normalize('NFKC', value or ''))
|
||||
return ' '.join(_CONTACT.sub(' ', value).split())[:30]
|
||||
|
||||
|
||||
@@ -45,9 +50,30 @@ class GuestSessionRequestSerializer(serializers.Serializer):
|
||||
deviceId = serializers.CharField(max_length=64)
|
||||
|
||||
|
||||
class OTPCodeField(serializers.CharField):
|
||||
"""
|
||||
کدِ ورود: ۴ تا ۶ رقم (کدِ پیامکی ۶ رقمی است، کدهای TEST_LOGIN_PHONES ۴ تا ۶). رقمِ فارسی و
|
||||
دیگر رقمهای یونیکد به لاتین برمیگردند؛ هر چیزِ دیگر ۴۰۰ است. بدونِ این، کدِ غیرِ ASCII برای
|
||||
شمارهی تست به secrets.compare_digest میرسید و TypeError (۵۰۰) میداد.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs.setdefault('max_length', 6)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
code = ascii_digits(super().to_internal_value(data))
|
||||
if not re.fullmatch(r'[0-9]{4,6}', code):
|
||||
raise serializers.ValidationError('کد باید ۴ تا ۶ رقم باشد.')
|
||||
return code
|
||||
|
||||
|
||||
class LoginRequestSerializer(serializers.Serializer):
|
||||
phone = IranMobileField()
|
||||
otp = serializers.CharField(max_length=6)
|
||||
otp = OTPCodeField()
|
||||
# فرانتِ فعلی نمیفرستد؛ اگر بیاید روی Session.device_id مینشیند. null یا بیش از ۶۴ نویسه ۴۰۰ است
|
||||
# (پیشتر null به NOT NULL میخورد و ۵۰۰ میداد).
|
||||
deviceId = serializers.CharField(max_length=64, required=False, allow_blank=True)
|
||||
|
||||
|
||||
class RefreshRequestSerializer(serializers.Serializer):
|
||||
@@ -57,6 +83,15 @@ class RefreshRequestSerializer(serializers.Serializer):
|
||||
class SetGradeRequestSerializer(serializers.Serializer):
|
||||
gradeId = serializers.IntegerField()
|
||||
|
||||
def validate_gradeId(self, value):
|
||||
# پایهی ناموجود ۴۰۰ است، نه ذخیرهی بیصدا (نقشهی خالی). عددِ بزرگتر از ستونِ SQLite هم
|
||||
# پیشتر هنگامِ ذخیره OverflowError (۵۰۰) میداد؛ exists() آن را بیخطا «نیست» میبیند.
|
||||
from content.models import Grade
|
||||
|
||||
if not Grade.objects.filter(pk=value).exists():
|
||||
raise serializers.ValidationError('این پایه وجود ندارد.')
|
||||
return value
|
||||
|
||||
|
||||
# همان بازهای که پیشتر برای «سن» پذیرفته میشد. بزرگسال هم میآید (سینیور،
|
||||
# ۲۰۲۶-۱۰-۰۱): کسی که فقط یک مبحثِ ریاضی را میخواهد یاد بگیرد.
|
||||
@@ -182,7 +217,7 @@ class ProfileResponseSerializer(serializers.Serializer):
|
||||
|
||||
class DeleteAccountRequestSerializer(serializers.Serializer):
|
||||
# کاربرِ شمارهدار: کدِ پیامکیِ تازه (از POST /api/auth/otp/request با همان شماره).
|
||||
otp = serializers.CharField(max_length=6, required=False, allow_blank=True)
|
||||
otp = OTPCodeField(required=False, allow_blank=True)
|
||||
# مهمان شمارهای ندارد که کد بگیرد؛ باید همین کلمه را تایپ کرده باشد.
|
||||
confirm = serializers.CharField(max_length=10, required=False, allow_blank=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user