security: harden webhooks and session secrets
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from bot.handlers.user.payment import yookassa_webhook_route
|
||||
from bot.services.freekassa_service import FreeKassaService
|
||||
from bot.utils.request_security import request_client_ip
|
||||
|
||||
|
||||
class RequestSecurityTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_request_client_ip_uses_last_forwarded_for_value_for_trusted_proxy(self):
|
||||
request = SimpleNamespace(
|
||||
remote="127.0.0.1",
|
||||
headers={"X-Forwarded-For": "203.0.113.10, 198.51.100.7"},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
request_client_ip(request, trusted_proxies=["127.0.0.1"]),
|
||||
"198.51.100.7",
|
||||
)
|
||||
|
||||
async def test_yookassa_webhook_rejects_untrusted_ip_before_reading_body(self):
|
||||
request = SimpleNamespace(
|
||||
app={
|
||||
"bot": object(),
|
||||
"i18n": object(),
|
||||
"settings": SimpleNamespace(trusted_proxies=["127.0.0.1"]),
|
||||
"panel_service": object(),
|
||||
"subscription_service": object(),
|
||||
"referral_service": object(),
|
||||
"lknpd_service": None,
|
||||
"async_session_factory": object(),
|
||||
},
|
||||
headers={},
|
||||
remote="203.0.113.50",
|
||||
json=AsyncMock(side_effect=AssertionError("request.json() must not be called")),
|
||||
)
|
||||
|
||||
response = await yookassa_webhook_route(request)
|
||||
|
||||
self.assertEqual(response.status, 403)
|
||||
request.json.assert_not_awaited()
|
||||
|
||||
|
||||
class FreeKassaServiceTests(unittest.TestCase):
|
||||
def _make_service(self) -> FreeKassaService:
|
||||
settings = SimpleNamespace(
|
||||
FREEKASSA_ENABLED=True,
|
||||
FREEKASSA_MERCHANT_ID="123456",
|
||||
FREEKASSA_API_KEY="api-key",
|
||||
FREEKASSA_SECOND_SECRET="second-secret",
|
||||
DEFAULT_CURRENCY_SYMBOL="RUB",
|
||||
FREEKASSA_PAYMENT_IP="203.0.113.10",
|
||||
FREEKASSA_PAYMENT_METHOD_ID=44,
|
||||
FREEKASSA_TRUSTED_IPS="127.0.0.1,203.0.113.0/24",
|
||||
trusted_proxies=["127.0.0.1"],
|
||||
freekassa_trusted_ips=["127.0.0.1", "203.0.113.0/24"],
|
||||
)
|
||||
return FreeKassaService(
|
||||
bot=object(),
|
||||
settings=settings,
|
||||
i18n=object(),
|
||||
async_session_factory=object(),
|
||||
subscription_service=object(),
|
||||
referral_service=object(),
|
||||
)
|
||||
|
||||
def test_validate_signature_accepts_hmac_sha256_raw_body(self):
|
||||
service = self._make_service()
|
||||
raw_body = b'{"amount":"199.00","o":"42"}'
|
||||
expected_signature = hmac.new(
|
||||
service.second_secret.encode("utf-8"),
|
||||
raw_body,
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
self.assertTrue(service._validate_signature(raw_body, expected_signature))
|
||||
|
||||
def test_validate_signature_rejects_wrong_signature(self):
|
||||
service = self._make_service()
|
||||
|
||||
self.assertFalse(service._validate_signature(b"payload", "not-a-signature"))
|
||||
|
||||
def test_webhook_rejects_unauthorized_ip_before_body_read(self):
|
||||
service = self._make_service()
|
||||
request = SimpleNamespace(
|
||||
remote="198.51.100.250",
|
||||
headers={},
|
||||
read=AsyncMock(side_effect=AssertionError("request.read() must not be called")),
|
||||
)
|
||||
|
||||
response = asyncio_run(service.webhook_route(request))
|
||||
|
||||
self.assertEqual(response.status, 403)
|
||||
request.read.assert_not_awaited()
|
||||
|
||||
|
||||
def asyncio_run(coro):
|
||||
import asyncio
|
||||
|
||||
return asyncio.run(coro)
|
||||
Reference in New Issue
Block a user