Log trusted forwarded client IPs

This commit is contained in:
BADtochka
2026-06-06 00:37:03 +03:00
parent 766f2a5780
commit cc74ddec10
2 changed files with 63 additions and 2 deletions
+41 -2
View File
@@ -1,13 +1,16 @@
import asyncio
import functools
import hmac
import logging
from aiogram import Bot, Dispatcher
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiohttp import web
from aiohttp.web_log import AccessLogger, KeyMethod
from sqlalchemy.orm import sessionmaker
from bot.payment_providers import iter_provider_specs, iter_service_keys
from bot.utils.request_security import request_client_ip
from config.settings import Settings
@@ -18,6 +21,39 @@ class SecureSimpleRequestHandler(SimpleRequestHandler):
return hmac.compare_digest(telegram_secret_token, self.secret_token)
class TrustedProxyAccessLogger(AccessLogger):
"""Aiohttp access logger that respects trusted X-Forwarded-For headers."""
def compile_format(self, log_format):
methods = []
for atom in self.FORMAT_RE.findall(log_format):
if atom[1] == "":
format_key = self.LOG_FORMAT_MAP[atom[0]]
method = getattr(type(self), f"_format_{atom[0]}", None)
if method is None:
method = getattr(AccessLogger, f"_format_{atom[0]}")
methods.append(KeyMethod(format_key, method))
else:
format_key = (self.LOG_FORMAT_MAP[atom[2]], atom[1])
method = getattr(type(self), f"_format_{atom[2]}", None)
if method is None:
method = getattr(AccessLogger, f"_format_{atom[2]}")
methods.append(KeyMethod(format_key, functools.partial(method, atom[1])))
compiled = self.FORMAT_RE.sub(r"%s", log_format)
compiled = self.CLEANUP_RE.sub(r"%\1", compiled)
return compiled, methods
@staticmethod
def _format_a(request, response, time):
if request is None:
return "-"
settings = request.app.get("settings") if hasattr(request, "app") else None
trusted_proxies = getattr(settings, "trusted_proxies", None)
client_ip = request_client_ip(request, trusted_proxies=trusted_proxies)
return client_ip or "-"
def _inject_shared_instances(
app: web.Application,
dp: Dispatcher,
@@ -110,7 +146,7 @@ async def build_and_start_web_app(
runners = []
webhooks_runner = web.AppRunner(app)
webhooks_runner = web.AppRunner(app, access_log_class=TrustedProxyAccessLogger)
await webhooks_runner.setup()
runners.append(webhooks_runner)
site = web.TCPSite(
@@ -133,7 +169,10 @@ async def build_and_start_web_app(
settings,
async_session_factory,
)
subscription_runner = web.AppRunner(subscription_app)
subscription_runner = web.AppRunner(
subscription_app,
access_log_class=TrustedProxyAccessLogger,
)
await subscription_runner.setup()
runners.append(subscription_runner)
subscription_site = web.TCPSite(
+22
View File
@@ -11,6 +11,7 @@ from aiohttp import web
from bot.app.web import admin_api, subscription_webapp
from bot.app.web.admin_api_impl import settings as admin_settings_routes
from bot.app.web.web_server import TrustedProxyAccessLogger
from bot.app.web.webapp import account as account_routes
from bot.app.web.webapp_auth import (
create_telegram_oauth_nonce,
@@ -47,6 +48,27 @@ class RequestSecurityTests(unittest.IsolatedAsyncioTestCase):
"198.51.100.7",
)
async def test_access_logger_uses_forwarded_ip_only_for_trusted_proxy(self):
trusted_request = SimpleNamespace(
remote="172.19.0.7",
headers={"X-Forwarded-For": "203.0.113.10"},
app={"settings": SimpleNamespace(trusted_proxies=["172.19.0.0/16"])},
)
untrusted_request = SimpleNamespace(
remote="172.19.0.7",
headers={"X-Forwarded-For": "203.0.113.10"},
app={"settings": SimpleNamespace(trusted_proxies=["127.0.0.1"])},
)
self.assertEqual(
TrustedProxyAccessLogger._format_a(trusted_request, object(), 0),
"203.0.113.10",
)
self.assertEqual(
TrustedProxyAccessLogger._format_a(untrusted_request, object(), 0),
"172.19.0.7",
)
async def test_yookassa_webhook_rejects_untrusted_ip_before_reading_body(self):
request = SimpleNamespace(
app={