From 710fbcc8d26e8fa437ea8f90c002e102d2fc3725 Mon Sep 17 00:00:00 2001 From: Squ1reX Date: Wed, 27 May 2026 18:52:43 +0300 Subject: [PATCH 1/8] fix(auth): Fix a vulnerability in the process of linking a Telegram bot to an account by adding X-Bot-Secret to the header --- src/config.py | 1 + src/presentation/api/v1/auth/tg_link.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/config.py b/src/config.py index 366ae92..04b4010 100644 --- a/src/config.py +++ b/src/config.py @@ -4,6 +4,7 @@ load_dotenv() TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") +BOT_SECRET = os.getenv("BOT_SECRET") DATABASE_URL = os.getenv("DATABASE_URL") diff --git a/src/presentation/api/v1/auth/tg_link.py b/src/presentation/api/v1/auth/tg_link.py index 56746ca..f723cce 100644 --- a/src/presentation/api/v1/auth/tg_link.py +++ b/src/presentation/api/v1/auth/tg_link.py @@ -1,5 +1,6 @@ -from datetime import datetime, timezone, timedelta import secrets + +from datetime import datetime, timezone, timedelta from fastapi import Request, Depends, APIRouter from slowapi import Limiter from slowapi.util import get_remote_address @@ -7,8 +8,9 @@ from src.infrastructure.db.database import get_db from src.domain.services.tg_link_service import TgLinkService -from src.presentation.api.v1.exceptions import NotCorrect +from src.presentation.api.v1.exceptions import NotCorrect, NoAccess from src.logger import logger +from src.config import BOT_SECRET router = APIRouter(prefix="/auth", tags=["Магические ссылки"]) limiter = Limiter(key_func=get_remote_address) @@ -28,9 +30,13 @@ async def create_telegram_magic_link( """ logger.debug(f"Получен tg_id: {telegram_id}") + bot_secret_header = request.headers.get("X-Bot-Secret") + if bot_secret_header != BOT_SECRET: + raise NoAccess("Недостаточно прав для доступа к этому ресурсу") + if await TgLinkService.check_telegram_connection(telegram_id, db): logger.warning(f"Этот аккаунт телеграм уже привязан к платформе: {telegram_id}") - raise NotCorrect("Уже привязан") + raise NotCorrect("Этот аккаунт телеграм уже привязан к платформе") token = secrets.token_urlsafe(32) expires = datetime.now(timezone.utc) + timedelta(minutes=10) From a6f8b3605715392912782a68452c6e7eb1da2a69 Mon Sep 17 00:00:00 2001 From: Squ1reX Date: Wed, 27 May 2026 20:43:34 +0300 Subject: [PATCH 2/8] chore(auth): Improve logging of the create_telegram_magic_link function --- src/presentation/api/v1/auth/tg_link.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/presentation/api/v1/auth/tg_link.py b/src/presentation/api/v1/auth/tg_link.py index f723cce..ef91bde 100644 --- a/src/presentation/api/v1/auth/tg_link.py +++ b/src/presentation/api/v1/auth/tg_link.py @@ -28,12 +28,15 @@ async def create_telegram_magic_link( db: AsyncSession -> dict """ - logger.debug(f"Получен tg_id: {telegram_id}") + logger.debug(f"Получен запрос на привязку аккаунта к боту tg_id: {telegram_id}") + logger.info("Проверка заголовка X-Bot-Secret...") bot_secret_header = request.headers.get("X-Bot-Secret") if bot_secret_header != BOT_SECRET: + logger.info("Заголовок X-Bot-Secret не совпадает с секретным ключем") raise NoAccess("Недостаточно прав для доступа к этому ресурсу") + logger.info("Проверка наличия привязки аккаунта к платформе...") if await TgLinkService.check_telegram_connection(telegram_id, db): logger.warning(f"Этот аккаунт телеграм уже привязан к платформе: {telegram_id}") raise NotCorrect("Этот аккаунт телеграм уже привязан к платформе") @@ -43,6 +46,7 @@ async def create_telegram_magic_link( logger.info("Сохранение magic токена...") await TgLinkService.save_link_token(token=token, expires_at=expires, db=db, telegram_id=telegram_id) + logger.info("Токен успешно сохранен!") return {"token": token} From 6a42fa015cd9a4632595977b2a7ac702faa506d9 Mon Sep 17 00:00:00 2001 From: Squ1reX Date: Wed, 27 May 2026 21:49:43 +0300 Subject: [PATCH 3/8] fix(auth): Fix X-Bot-Secret authentication. Eliminates timig attack. --- src/config.py | 2 +- src/domain/services/tg_link_service.py | 21 +++++++++++++++++++++ src/presentation/api/v1/auth/tg_link.py | 15 ++++++++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/config.py b/src/config.py index 04b4010..239c13e 100644 --- a/src/config.py +++ b/src/config.py @@ -4,7 +4,7 @@ load_dotenv() TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") -BOT_SECRET = os.getenv("BOT_SECRET") +BOT_SECRET = os.environ["BOT_SECRET"] DATABASE_URL = os.getenv("DATABASE_URL") diff --git a/src/domain/services/tg_link_service.py b/src/domain/services/tg_link_service.py index 9ed00ed..fd9e4eb 100644 --- a/src/domain/services/tg_link_service.py +++ b/src/domain/services/tg_link_service.py @@ -1,11 +1,32 @@ +import hmac + from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from datetime import datetime, timezone from src.infrastructure.db.models import User, MagicToken +from src.config import BOT_SECRET from src.logger import logger class TgLinkService: + @staticmethod + def verify_bot_secret_key(bot_secret_header: str): + """ + Сравниваем секретный ключ с полученным ключем в заголовке. + Устранена возможность timig атаки. + """ + if not BOT_SECRET: + raise RuntimeError("Не задан секретный ключ X-Bot-Secret") + + result = hmac.compare_digest( + bot_secret_header.encode(), + BOT_SECRET.encode() + ) + if not result: + logger.warning("Не верный ключ в X-Bot-Secret. Возможна попытка атаки!") + + return result + @staticmethod async def check_telegram_connection(tg_id: int, db: AsyncSession) -> User | None: """ diff --git a/src/presentation/api/v1/auth/tg_link.py b/src/presentation/api/v1/auth/tg_link.py index ef91bde..a510c72 100644 --- a/src/presentation/api/v1/auth/tg_link.py +++ b/src/presentation/api/v1/auth/tg_link.py @@ -10,7 +10,6 @@ from src.domain.services.tg_link_service import TgLinkService from src.presentation.api.v1.exceptions import NotCorrect, NoAccess from src.logger import logger -from src.config import BOT_SECRET router = APIRouter(prefix="/auth", tags=["Магические ссылки"]) limiter = Limiter(key_func=get_remote_address) @@ -32,8 +31,16 @@ async def create_telegram_magic_link( logger.info("Проверка заголовка X-Bot-Secret...") bot_secret_header = request.headers.get("X-Bot-Secret") - if bot_secret_header != BOT_SECRET: - logger.info("Заголовок X-Bot-Secret не совпадает с секретным ключем") + + if not bot_secret_header: + logger.info("В заголовке отсутствует секретный ключ") + raise NoAccess("Недостаточно прав для доступа к этому ресурсу") + + logger.info("Сравнение ключей...") + verify_key = TgLinkService.verify_bot_secret_key(bot_secret_header) + + if not verify_key: + logger.info("Предоставленый ключ не совпадает с секретным ключем") raise NoAccess("Недостаточно прав для доступа к этому ресурсу") logger.info("Проверка наличия привязки аккаунта к платформе...") @@ -44,6 +51,8 @@ async def create_telegram_magic_link( token = secrets.token_urlsafe(32) expires = datetime.now(timezone.utc) + timedelta(minutes=10) + #! TODO: Добавить изоляцию на уровне docker + logger.info("Сохранение magic токена...") await TgLinkService.save_link_token(token=token, expires_at=expires, db=db, telegram_id=telegram_id) logger.info("Токен успешно сохранен!") From 272af0076583147f0f7c4a1773430ee77158d92e Mon Sep 17 00:00:00 2001 From: Squ1reX Date: Wed, 27 May 2026 21:50:27 +0300 Subject: [PATCH 4/8] chore(requirements): update requrements.txt list --- requirements.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/requirements.txt b/requirements.txt index edf0f20..e2dc44e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ annotated-doc==0.0.4 annotated-types==0.7.0 anyio==4.12.0 APScheduler==3.11.2 +argcomplete==3.6.3 argon2-cffi==25.1.0 argon2-cffi-bindings==25.1.0 async-lru==2.1.0 @@ -22,9 +23,11 @@ cffi==2.0.0 charset-normalizer==3.4.4 click==8.3.1 colorama==0.4.6 +commitizen==4.13.9 coverage==7.13.4 cryptography==46.0.5 cyclonedx-python-lib==11.6.0 +decli==0.6.3 defusedxml==0.7.1 Deprecated==1.3.1 ecdsa==0.19.1 @@ -38,6 +41,7 @@ httpx==0.28.1 idna==3.11 iniconfig==2.3.0 itsdangerous==2.2.0 +Jinja2==3.1.6 librt==0.7.8 license-expression==30.4.4 limits==5.6.0 @@ -61,6 +65,7 @@ pip-requirements-parser==32.0.1 pip_audit==2.10.0 platformdirs==4.9.4 pluggy==1.6.0 +prompt_toolkit==3.0.51 propcache==0.4.1 py-serializable==2.1.0 pyasn1==0.6.2 @@ -81,6 +86,7 @@ python-multipart==0.0.22 python-socks==2.8.0 pytz==2025.2 PyYAML==6.0.3 +questionary==2.1.1 requests==2.32.5 rich==14.3.3 rsa==4.9.1 @@ -91,8 +97,10 @@ sortedcontainers==2.4.0 SQLAlchemy==2.0.45 starlette==0.50.0 stevedore==5.7.0 +termcolor==3.3.0 tomli==2.4.0 tomli_w==1.2.0 +tomlkit==0.14.0 tqdm==4.67.3 typing-inspection==0.4.2 typing_extensions==4.15.0 @@ -100,5 +108,6 @@ tzdata==2025.3 tzlocal==5.3.1 urllib3==2.6.3 uvicorn==0.40.0 +wcwidth==0.6.0 wrapt==2.0.1 yarl==1.22.0 From a6b5812297d8a2718d514a413d812fd6fe2b8310 Mon Sep 17 00:00:00 2001 From: Squ1reX Date: Wed, 27 May 2026 22:03:18 +0300 Subject: [PATCH 5/8] fix(bot): Add sending of the X-Bot-Secret header from the Telegram bot --- src/domain/services/tg_client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/domain/services/tg_client.py b/src/domain/services/tg_client.py index 62a41c7..ed15c9d 100644 --- a/src/domain/services/tg_client.py +++ b/src/domain/services/tg_client.py @@ -3,6 +3,7 @@ from src.logger import logger from src.shared.schemas.bot.tg_link import TgLinkStatus +from src.config import BOT_SECRET @alru_cache(maxsize=512, ttl=300) # кэширование, хранить максимум 512 результатов, удалсять через 300 секунд async def check_registration(user_id: str) -> TgLinkStatus: @@ -52,8 +53,12 @@ async def generate_magic_token(tg_id: int) -> str | None: """Отправляет запрос к api на создание magic токена""" try: async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, read=10.0)) as client: - logger.info(f"Отвправка запроса на генерацию magic токна для пользователя: {tg_id}...") - response = await client.get(f"http://api:8000/api/v1/auth/telegram/generate-link/{tg_id}") + logger.info(f"Отправка запроса на генерацию magic токна для пользователя: {tg_id}...") + + headers = { + "X-Bot-Secret": BOT_SECRET + } + response = await client.get(f"http://api:8000/api/v1/auth/telegram/generate-link/{tg_id}", headers=headers) if response.status_code == 200: data = response.json() From 1e2a6dd436c7c79267b64686bcec60a43abec164 Mon Sep 17 00:00:00 2001 From: Squ1reX Date: Fri, 29 May 2026 16:18:25 +0300 Subject: [PATCH 6/8] fix(config): Remove os.environ, which causes an error in tests --- src/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.py b/src/config.py index 239c13e..04b4010 100644 --- a/src/config.py +++ b/src/config.py @@ -4,7 +4,7 @@ load_dotenv() TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") -BOT_SECRET = os.environ["BOT_SECRET"] +BOT_SECRET = os.getenv("BOT_SECRET") DATABASE_URL = os.getenv("DATABASE_URL") From 08bfa0404f1a5d5c33c25bc527263ccb2f325ac9 Mon Sep 17 00:00:00 2001 From: Squ1reX <82816845+Fl1riX@users.noreply.github.com> Date: Fri, 29 May 2026 16:19:49 +0300 Subject: [PATCH 7/8] Update comment src/presentation/api/v1/auth/tg_link.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/presentation/api/v1/auth/tg_link.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/presentation/api/v1/auth/tg_link.py b/src/presentation/api/v1/auth/tg_link.py index a510c72..90381cc 100644 --- a/src/presentation/api/v1/auth/tg_link.py +++ b/src/presentation/api/v1/auth/tg_link.py @@ -38,9 +38,9 @@ async def create_telegram_magic_link( logger.info("Сравнение ключей...") verify_key = TgLinkService.verify_bot_secret_key(bot_secret_header) - + if not verify_key: - logger.info("Предоставленый ключ не совпадает с секретным ключем") + logger.info("Предоставленный ключ не совпадает с секретным ключом") raise NoAccess("Недостаточно прав для доступа к этому ресурсу") logger.info("Проверка наличия привязки аккаунта к платформе...") From eda398a7ccb63ab5ba811e246071fb1eb97545f8 Mon Sep 17 00:00:00 2001 From: Squ1reX Date: Fri, 29 May 2026 16:23:21 +0300 Subject: [PATCH 8/8] docs(services): Fix logging and doc string errors --- src/domain/services/tg_link_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/services/tg_link_service.py b/src/domain/services/tg_link_service.py index fd9e4eb..e61f06d 100644 --- a/src/domain/services/tg_link_service.py +++ b/src/domain/services/tg_link_service.py @@ -11,9 +11,9 @@ class TgLinkService: @staticmethod def verify_bot_secret_key(bot_secret_header: str): - """ - Сравниваем секретный ключ с полученным ключем в заголовке. - Устранена возможность timig атаки. + """ + Сравниваем секретный ключ с полученным ключом в заголовке. + Устранена возможность timing-атаки. """ if not BOT_SECRET: raise RuntimeError("Не задан секретный ключ X-Bot-Secret") @@ -23,7 +23,7 @@ def verify_bot_secret_key(bot_secret_header: str): BOT_SECRET.encode() ) if not result: - logger.warning("Не верный ключ в X-Bot-Secret. Возможна попытка атаки!") + logger.warning("Неверный ключ в X-Bot-Secret. Возможна попытка атаки!") return result