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)