mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-05 05:13:21 +00:00
fix: address security review findings
- Replace format_map with regex-based placeholder substitution to prevent format string injection via attribute traversal (CRITICAL) - Add UUID format validation in select_remna_config handler - Redact exception details from user-facing callback answers - HTML-escape current_uuid in admin config menu - HTML-escape title/description in format_additional_section
This commit is contained in:
@@ -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'✅ Текущий: <b>{html.escape(current_name)}</b>\n\n'
|
||||
else:
|
||||
text += f'⚠️ Текущий UUID не найден: <code>{current_uuid}</code>\n\n'
|
||||
text += f'⚠️ Текущий UUID не найден: <code>{html.escape(str(current_uuid))}</code>\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
|
||||
|
||||
@@ -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',
|
||||
'<b>{title}:</b>',
|
||||
).format(title=title)
|
||||
).format(title=html_mod.escape(title))
|
||||
)
|
||||
|
||||
if description:
|
||||
parts.append(description)
|
||||
parts.append(html_mod.escape(description))
|
||||
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user