diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index 27883c4c..2bb4fe9e 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -2704,7 +2704,8 @@ async def show_remna_config_menu(callback: types.CallbackQuery, db_user: User, d async with service.get_api_client() as api: configs = await api.get_subscription_page_configs() except Exception as e: - await callback.answer(f'Ошибка загрузки конфигов: {e}', show_alert=True) + logger.error('Failed to load Remnawave configs', error=e) + await callback.answer('Ошибка загрузки конфигов', show_alert=True) return keyboard: list[list[types.InlineKeyboardButton]] = [] @@ -2722,7 +2723,7 @@ async def show_remna_config_menu(callback: types.CallbackQuery, db_user: User, d if current_name: text += f'✅ Текущий: {html.escape(current_name)}\n\n' else: - text += f'⚠️ Текущий UUID не найден: {current_uuid}\n\n' + text += f'⚠️ Текущий UUID не найден: {html.escape(str(current_uuid))}\n\n' else: text += 'ℹ️ Конфиг не выбран (используется app-config.json)\n\n' @@ -2765,11 +2766,19 @@ async def select_remna_config(callback: types.CallbackQuery, db_user: User, db: """Select a Remnawave subscription page config.""" uuid = callback.data.replace('admin_remna_select_', '') + # Validate UUID format + import re as _re + + if not _re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', uuid): + await callback.answer('Некорректный UUID конфигурации', show_alert=True) + return + try: await bot_configuration_service.set_value(db, 'CABINET_REMNA_SUB_CONFIG', uuid) await db.commit() except Exception as e: - await callback.answer(f'Ошибка сохранения: {e}', show_alert=True) + logger.error('Failed to save Remnawave config UUID', error=e) + await callback.answer('Ошибка сохранения', show_alert=True) return # Invalidate app config cache @@ -2791,7 +2800,8 @@ async def clear_remna_config(callback: types.CallbackQuery, db_user: User, db: A await bot_configuration_service.set_value(db, 'CABINET_REMNA_SUB_CONFIG', '') await db.commit() except Exception as e: - await callback.answer(f'Ошибка сброса: {e}', show_alert=True) + logger.error('Failed to clear Remnawave config', error=e) + await callback.answer('Ошибка сброса', show_alert=True) return from app.handlers.subscription.common import invalidate_app_config_cache diff --git a/app/handlers/subscription/common.py b/app/handlers/subscription/common.py index 471b42f6..aa8e4ae5 100644 --- a/app/handlers/subscription/common.py +++ b/app/handlers/subscription/common.py @@ -2,6 +2,7 @@ import asyncio import base64 import html as html_mod import json +import re import time from datetime import datetime from typing import Any @@ -32,22 +33,28 @@ _app_config_cache_ts: float = 0.0 _app_config_lock = asyncio.Lock() -class _SafeFormatDict(dict): - def __missing__(self, key: str) -> str: # pragma: no cover - defensive fallback - return '{' + key + '}' +_PLACEHOLDER_RE = re.compile(r'\{(\w+)\}') def _format_text_with_placeholders(template: str, values: dict[str, Any]) -> str: + """Safe placeholder substitution — only replaces simple {key} patterns. + + Unlike str.format_map, this does NOT allow attribute access ({key.attr}) + or indexing ({key[0]}), preventing format string injection attacks. + """ if not isinstance(template, str): return template - safe_values = _SafeFormatDict() - safe_values.update(values) + def _replace(match: re.Match) -> str: + key = match.group(1) + if key in values: + return str(values[key]) + return match.group(0) try: - return template.format_map(safe_values) + return _PLACEHOLDER_RE.sub(_replace, template) except Exception: # pragma: no cover - defensive logging - logger.warning("Failed to format template '' with values", template=template, values=values) + logger.warning('Failed to format template with values', template=template, values=values) return template @@ -233,11 +240,11 @@ def format_additional_section(additional: Any, texts, language: str) -> str: texts.t( 'SUBSCRIPTION_ADDITIONAL_STEP_TITLE', '{title}:', - ).format(title=title) + ).format(title=html_mod.escape(title)) ) if description: - parts.append(description) + parts.append(html_mod.escape(description)) return '\n'.join(parts)