fix(security): timing attack, rate limiter за прокси, хардкод ID в скриптах (20.09.2026)
This commit is contained in:
@@ -58,7 +58,7 @@ async def verify_telegram_initdata(init_data: str):
|
|||||||
hashlib.sha256
|
hashlib.sha256
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
if calculated_hash != received_hash:
|
if not hmac.compare_digest(calculated_hash, received_hash):
|
||||||
raise HTTPException(401, "Invalid initData signature")
|
raise HTTPException(401, "Invalid initData signature")
|
||||||
|
|
||||||
# Извлекаем данные пользователя
|
# Извлекаем данные пользователя
|
||||||
|
|||||||
+39
-11
@@ -3,29 +3,57 @@
|
|||||||
Очищает ВСЕ данные по играм, урокам, чат-учителю, достижениям.
|
Очищает ВСЕ данные по играм, урокам, чат-учителю, достижениям.
|
||||||
Не трогает: users (остаётся), tts_cache (аудио).
|
Не трогает: users (остаётся), tts_cache (аудио).
|
||||||
|
|
||||||
Запуск: python3 full_reset.py
|
Запуск: python3 full_reset.py <USER_ID>
|
||||||
|
|
||||||
|
🔧 ФИКС (20.09.2026): убран хардкод USER_ID — теперь через аргумент + подтверждение.
|
||||||
|
Раньше: USER_ID = 6694989403 (мог случайно снести прогресс на проде).
|
||||||
|
Теперь: python3 full_reset.py 123456789 → запросит подтверждение "YES".
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
import asyncpg
|
import asyncpg
|
||||||
|
|
||||||
from config import DB_CONFIG
|
from config import DB_CONFIG
|
||||||
|
|
||||||
USER_ID = 6694989403
|
|
||||||
|
|
||||||
|
# ============ Проверка аргументов ============
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("❌ Использование: python3 full_reset.py <USER_ID>")
|
||||||
|
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():
|
async def main():
|
||||||
pool = await asyncpg.create_pool(**DB_CONFIG)
|
pool = await asyncpg.create_pool(**DB_CONFIG)
|
||||||
|
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
# Проверяем пользователя
|
# Проверяем пользователя
|
||||||
user_exists = await conn.fetchval("""
|
user_exists = await conn.fetchval("""
|
||||||
SELECT 1 FROM users WHERE telegram_id = $1
|
SELECT 1 FROM users WHERE telegram_id = $1
|
||||||
""", USER_ID)
|
""", USER_ID)
|
||||||
|
|
||||||
if not user_exists:
|
if not user_exists:
|
||||||
print(f"❌ Пользователь {USER_ID} не найден!")
|
print(f"❌ Пользователь {USER_ID} не найден!")
|
||||||
await pool.close()
|
await pool.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Список таблиц для очистки (по user_id)
|
# Список таблиц для очистки (по user_id)
|
||||||
user_tables = [
|
user_tables = [
|
||||||
"user_progress", # Филворды
|
"user_progress", # Филворды
|
||||||
@@ -46,9 +74,9 @@ async def main():
|
|||||||
"daily_logins", # Входы
|
"daily_logins", # Входы
|
||||||
"ai_user_context", # AI-контекст
|
"ai_user_context", # AI-контекст
|
||||||
]
|
]
|
||||||
|
|
||||||
total_deleted = 0
|
total_deleted = 0
|
||||||
|
|
||||||
for table in user_tables:
|
for table in user_tables:
|
||||||
try:
|
try:
|
||||||
result = await conn.execute(f"""
|
result = await conn.execute(f"""
|
||||||
@@ -59,14 +87,14 @@ async def main():
|
|||||||
print(f"🗑️ {table}: удалено {affected}")
|
print(f"🗑️ {table}: удалено {affected}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ {table}: {str(e)}")
|
print(f"⚠️ {table}: {str(e)}")
|
||||||
|
|
||||||
# Сбрасываем last_completed_level
|
# Сбрасываем last_completed_level
|
||||||
await conn.execute("""
|
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
|
WHERE telegram_id = $1
|
||||||
""", USER_ID)
|
""", USER_ID)
|
||||||
print("✅ last_completed_level = 0, current_level = A1")
|
print("✅ last_completed_level = 0, current_level = A1")
|
||||||
|
|
||||||
# Очищаем архив ответов
|
# Очищаем архив ответов
|
||||||
try:
|
try:
|
||||||
result = await conn.execute("""
|
result = await conn.execute("""
|
||||||
@@ -75,7 +103,7 @@ async def main():
|
|||||||
print(f"🗑️ user_learning_answers_archive: удалено {result.split()[-1]}")
|
print(f"🗑️ user_learning_answers_archive: удалено {result.split()[-1]}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ archive: {str(e)}")
|
print(f"⚠️ archive: {str(e)}")
|
||||||
|
|
||||||
await pool.close()
|
await pool.close()
|
||||||
print(f"\n🎉 Готово! Удалено {total_deleted} записей.")
|
print(f"\n🎉 Готово! Удалено {total_deleted} записей.")
|
||||||
print("Можно начинать тестирование с чистого листа!")
|
print("Можно начинать тестирование с чистого листа!")
|
||||||
|
|||||||
@@ -128,8 +128,16 @@ async def rate_limit_middleware(request: Request, call_next):
|
|||||||
if path in ["/", "/health"]:
|
if path in ["/", "/health"]:
|
||||||
return await call_next(request)
|
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"
|
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):
|
if not rate_limiter.is_allowed(rate_key, path):
|
||||||
retry_after = rate_limiter.get_retry_after(rate_key, path)
|
retry_after = rate_limiter.get_retry_after(rate_key, path)
|
||||||
|
|||||||
+38
-10
@@ -3,30 +3,58 @@
|
|||||||
Очищает: user_learning_answers, user_lesson_progress, user_topic_progress,
|
Очищает: user_learning_answers, user_lesson_progress, user_topic_progress,
|
||||||
user_skill_mastery, learning_sessions, teacher_chat_history, teacher_ai_usage.
|
user_skill_mastery, learning_sessions, teacher_chat_history, teacher_ai_usage.
|
||||||
|
|
||||||
Запуск: python3 reset_my_progress.py
|
Запуск: python3 reset_my_progress.py <USER_ID>
|
||||||
|
|
||||||
|
🔧 ФИКС (20.09.2026): убран хардкод USER_ID — теперь через аргумент + подтверждение.
|
||||||
|
Раньше: USER_ID = 6694989403 (мог случайно снести прогресс на проде).
|
||||||
|
Теперь: python3 reset_my_progress.py 123456789 → запросит подтверждение "YES".
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
import asyncpg
|
import asyncpg
|
||||||
|
|
||||||
from config import DB_CONFIG
|
from config import DB_CONFIG
|
||||||
|
|
||||||
# ID пользователя для сброса (твой Telegram ID)
|
|
||||||
USER_ID = 6694989403
|
|
||||||
|
|
||||||
|
# ============ Проверка аргументов ============
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("❌ Использование: python3 reset_my_progress.py <USER_ID>")
|
||||||
|
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():
|
async def main():
|
||||||
pool = await asyncpg.create_pool(**DB_CONFIG)
|
pool = await asyncpg.create_pool(**DB_CONFIG)
|
||||||
|
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
# Проверяем, что пользователь существует
|
# Проверяем, что пользователь существует
|
||||||
user_exists = await conn.fetchval("""
|
user_exists = await conn.fetchval("""
|
||||||
SELECT 1 FROM users WHERE telegram_id = $1
|
SELECT 1 FROM users WHERE telegram_id = $1
|
||||||
""", USER_ID)
|
""", USER_ID)
|
||||||
|
|
||||||
if not user_exists:
|
if not user_exists:
|
||||||
print(f"❌ Пользователь {USER_ID} не найден!")
|
print(f"❌ Пользователь {USER_ID} не найден!")
|
||||||
await pool.close()
|
await pool.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Список таблиц для очистки
|
# Список таблиц для очистки
|
||||||
tables = [
|
tables = [
|
||||||
"user_learning_answers",
|
"user_learning_answers",
|
||||||
@@ -37,26 +65,26 @@ async def main():
|
|||||||
"teacher_chat_history",
|
"teacher_chat_history",
|
||||||
"teacher_ai_usage",
|
"teacher_ai_usage",
|
||||||
]
|
]
|
||||||
|
|
||||||
for table in tables:
|
for table in tables:
|
||||||
result = await conn.execute(f"""
|
result = await conn.execute(f"""
|
||||||
DELETE FROM {table} WHERE user_id = $1
|
DELETE FROM {table} WHERE user_id = $1
|
||||||
""", USER_ID)
|
""", USER_ID)
|
||||||
affected = int(result.split()[-1])
|
affected = int(result.split()[-1])
|
||||||
print(f"🗑️ {table}: удалено {affected} записей")
|
print(f"🗑️ {table}: удалено {affected} записей")
|
||||||
|
|
||||||
# Также очищаем ai_user_context
|
# Также очищаем ai_user_context
|
||||||
result = await conn.execute("""
|
result = await conn.execute("""
|
||||||
DELETE FROM ai_user_context WHERE user_id = $1
|
DELETE FROM ai_user_context WHERE user_id = $1
|
||||||
""", USER_ID)
|
""", USER_ID)
|
||||||
print(f"🗑️ ai_user_context: удалено {result.split()[-1]} записей")
|
print(f"🗑️ ai_user_context: удалено {result.split()[-1]} записей")
|
||||||
|
|
||||||
# Сбрасываем last_completed_level
|
# Сбрасываем last_completed_level
|
||||||
await conn.execute("""
|
await conn.execute("""
|
||||||
UPDATE users SET last_completed_level = 0 WHERE telegram_id = $1
|
UPDATE users SET last_completed_level = 0 WHERE telegram_id = $1
|
||||||
""", USER_ID)
|
""", USER_ID)
|
||||||
print("✅ last_completed_level сброшен на 0")
|
print("✅ last_completed_level сброшен на 0")
|
||||||
|
|
||||||
await pool.close()
|
await pool.close()
|
||||||
print("\n🎉 Готово! Можно начинать заново!")
|
print("\n🎉 Готово! Можно начинать заново!")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user