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:
Fringg
2026-02-24 05:19:57 +03:00
parent fae6f71def
commit 6feec1eaa8
2 changed files with 30 additions and 13 deletions

View File

@@ -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

View File

@@ -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)