From e15a228f80e6ad8da42c5064a140ed9f33429617 Mon Sep 17 00:00:00 2001 From: Drobysh Date: Sun, 20 Sep 2026 17:49:58 +0000 Subject: [PATCH] =?UTF-8?q?fix(security):=20timing=20attack,=20rate=20limi?= =?UTF-8?q?ter=20=D0=B7=D0=B0=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8,=20?= =?UTF-8?q?=D1=85=D0=B0=D1=80=D0=B4=D0=BA=D0=BE=D0=B4=20ID=20=D0=B2=20?= =?UTF-8?q?=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=B0=D1=85=20(20.09.2026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth.py | 2 +- full_reset.py | 50 ++++++++++++++++++++++++++++++++++---------- main.py | 10 ++++++++- reset_my_progress.py | 48 +++++++++++++++++++++++++++++++++--------- 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/auth.py b/auth.py index da948af..8bc1d94 100644 --- a/auth.py +++ b/auth.py @@ -58,7 +58,7 @@ async def verify_telegram_initdata(init_data: str): hashlib.sha256 ).hexdigest() - if calculated_hash != received_hash: + if not hmac.compare_digest(calculated_hash, received_hash): raise HTTPException(401, "Invalid initData signature") # Извлекаем данные пользователя diff --git a/full_reset.py b/full_reset.py index 5f6822a..9e291c9 100644 --- a/full_reset.py +++ b/full_reset.py @@ -3,29 +3,57 @@ Очищает ВСЕ данные по играм, урокам, чат-учителю, достижениям. Не трогает: users (остаётся), tts_cache (аудио). -Запуск: python3 full_reset.py +Запуск: python3 full_reset.py + +🔧 ФИКС (20.09.2026): убран хардкод USER_ID — теперь через аргумент + подтверждение. +Раньше: USER_ID = 6694989403 (мог случайно снести прогресс на проде). +Теперь: python3 full_reset.py 123456789 → запросит подтверждение "YES". """ import asyncio +import sys + import asyncpg + from config import DB_CONFIG -USER_ID = 6694989403 +# ============ Проверка аргументов ============ + +if len(sys.argv) < 2: + print("❌ Использование: python3 full_reset.py ") + print(" Пример: python3 full_reset.py 6694989403") + sys.exit(1) + +try: + USER_ID = int(sys.argv[1]) +except ValueError: + print(f"❌ USER_ID должен быть числом, получено: {sys.argv[1]}") + sys.exit(1) + +confirm = input( + f"⚠️ Удалить ВСЕ данные пользователя {USER_ID}? Напиши YES для подтверждения: " +) +if confirm != "YES": + print("❌ Отменено.") + sys.exit(0) + + +# ============ Основная логика ============ async def main(): pool = await asyncpg.create_pool(**DB_CONFIG) - + async with pool.acquire() as conn: # Проверяем пользователя user_exists = await conn.fetchval(""" SELECT 1 FROM users WHERE telegram_id = $1 """, USER_ID) - + if not user_exists: print(f"❌ Пользователь {USER_ID} не найден!") await pool.close() return - + # Список таблиц для очистки (по user_id) user_tables = [ "user_progress", # Филворды @@ -46,9 +74,9 @@ async def main(): "daily_logins", # Входы "ai_user_context", # AI-контекст ] - + total_deleted = 0 - + for table in user_tables: try: result = await conn.execute(f""" @@ -59,14 +87,14 @@ async def main(): print(f"🗑️ {table}: удалено {affected}") except Exception as e: print(f"⚠️ {table}: {str(e)}") - + # Сбрасываем last_completed_level await conn.execute(""" - UPDATE users SET last_completed_level = 0, current_level = 'A1' + UPDATE users SET last_completed_level = 0, current_level = 'A1' WHERE telegram_id = $1 """, USER_ID) print("✅ last_completed_level = 0, current_level = A1") - + # Очищаем архив ответов try: result = await conn.execute(""" @@ -75,7 +103,7 @@ async def main(): print(f"🗑️ user_learning_answers_archive: удалено {result.split()[-1]}") except Exception as e: print(f"⚠️ archive: {str(e)}") - + await pool.close() print(f"\n🎉 Готово! Удалено {total_deleted} записей.") print("Можно начинать тестирование с чистого листа!") diff --git a/main.py b/main.py index 25a54f9..5d9ebad 100644 --- a/main.py +++ b/main.py @@ -128,8 +128,16 @@ async def rate_limit_middleware(request: Request, call_next): if path in ["/", "/health"]: return await call_next(request) + # 🔧 ФИКС (20.09.2026): за Cloudflare/nginx request.client.host — IP прокси. +# Берём реальный IP из X-Forwarded-For (первый адрес). +forwarded = request.headers.get("x-forwarded-for") +if forwarded: + client_ip = forwarded.split(",")[0].strip() +elif request.headers.get("cf-connecting-ip"): + client_ip = request.headers.get("cf-connecting-ip") +else: client_ip = request.client.host if request.client else "unknown" - rate_key = f"{client_ip}:{path}" +rate_key = f"{client_ip}:{path}" if not rate_limiter.is_allowed(rate_key, path): retry_after = rate_limiter.get_retry_after(rate_key, path) diff --git a/reset_my_progress.py b/reset_my_progress.py index ba76682..f13a7cf 100644 --- a/reset_my_progress.py +++ b/reset_my_progress.py @@ -3,30 +3,58 @@ Очищает: user_learning_answers, user_lesson_progress, user_topic_progress, user_skill_mastery, learning_sessions, teacher_chat_history, teacher_ai_usage. -Запуск: python3 reset_my_progress.py +Запуск: python3 reset_my_progress.py + +🔧 ФИКС (20.09.2026): убран хардкод USER_ID — теперь через аргумент + подтверждение. +Раньше: USER_ID = 6694989403 (мог случайно снести прогресс на проде). +Теперь: python3 reset_my_progress.py 123456789 → запросит подтверждение "YES". """ import asyncio +import sys + import asyncpg + from config import DB_CONFIG -# ID пользователя для сброса (твой Telegram ID) -USER_ID = 6694989403 +# ============ Проверка аргументов ============ + +if len(sys.argv) < 2: + print("❌ Использование: python3 reset_my_progress.py ") + print(" Пример: python3 reset_my_progress.py 6694989403") + sys.exit(1) + +try: + USER_ID = int(sys.argv[1]) +except ValueError: + print(f"❌ USER_ID должен быть числом, получено: {sys.argv[1]}") + sys.exit(1) + +confirm = input( + f"⚠️ Сбросить прогресс «Учитель» и «Уроки» для пользователя {USER_ID}? " + f"Напиши YES для подтверждения: " +) +if confirm != "YES": + print("❌ Отменено.") + sys.exit(0) + + +# ============ Основная логика ============ async def main(): pool = await asyncpg.create_pool(**DB_CONFIG) - + async with pool.acquire() as conn: # Проверяем, что пользователь существует user_exists = await conn.fetchval(""" SELECT 1 FROM users WHERE telegram_id = $1 """, USER_ID) - + if not user_exists: print(f"❌ Пользователь {USER_ID} не найден!") await pool.close() return - + # Список таблиц для очистки tables = [ "user_learning_answers", @@ -37,26 +65,26 @@ async def main(): "teacher_chat_history", "teacher_ai_usage", ] - + for table in tables: result = await conn.execute(f""" DELETE FROM {table} WHERE user_id = $1 """, USER_ID) affected = int(result.split()[-1]) print(f"🗑️ {table}: удалено {affected} записей") - + # Также очищаем ai_user_context result = await conn.execute(""" DELETE FROM ai_user_context WHERE user_id = $1 """, USER_ID) print(f"🗑️ ai_user_context: удалено {result.split()[-1]} записей") - + # Сбрасываем last_completed_level await conn.execute(""" UPDATE users SET last_completed_level = 0 WHERE telegram_id = $1 """, USER_ID) print("✅ last_completed_level сброшен на 0") - + await pool.close() print("\n🎉 Готово! Можно начинать заново!")