security: double-check CryptoPay webhook signatures

This commit is contained in:
3252a8
2026-04-26 20:33:49 +03:00
parent 5ec179b6d6
commit 53dcc59770
+23
View File
@@ -1,5 +1,7 @@
import hashlib
import logging import logging
import json import json
import hmac
from typing import Optional from typing import Optional
from aiogram import Bot from aiogram import Bot
@@ -19,6 +21,8 @@ from db.dal import payment_dal, user_dal
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
from bot.utils.config_link import prepare_config_links from bot.utils.config_link import prepare_config_links
logger = logging.getLogger(__name__)
class CryptoPayService: class CryptoPayService:
def __init__( def __init__(
@@ -38,6 +42,7 @@ class CryptoPayService:
self.async_session_factory = async_session_factory self.async_session_factory = async_session_factory
self.subscription_service = subscription_service self.subscription_service = subscription_service
self.referral_service = referral_service self.referral_service = referral_service
self.token = token
if token: if token:
net = Networks.TEST_NET if str(network).lower() == "testnet" else Networks.MAIN_NET net = Networks.TEST_NET if str(network).lower() == "testnet" else Networks.MAIN_NET
self.client = AioCryptoPay(token=token, network=net) self.client = AioCryptoPay(token=token, network=net)
@@ -261,9 +266,27 @@ class CryptoPayService:
except Exception as e: except Exception as e:
logging.error(f"Failed to send crypto_pay payment notification: {e}") logging.error(f"Failed to send crypto_pay payment notification: {e}")
def _validate_webhook_signature(self, raw_body: bytes, signature: str) -> bool:
if not self.token:
return False
expected_signature = hmac.new(
hashlib.sha256(self.token.encode("utf-8")).digest(),
raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected_signature, signature or ""):
logger.error("CryptoPay signature mismatch")
return False
return True
async def webhook_route(self, request: web.Request) -> web.Response: async def webhook_route(self, request: web.Request) -> web.Response:
if not self.configured or not self.client: if not self.configured or not self.client:
return web.Response(status=503, text="cryptopay_disabled") return web.Response(status=503, text="cryptopay_disabled")
raw_body = await request.read()
signature = request.headers.get("crypto-pay-api-signature", "")
if not self._validate_webhook_signature(raw_body, signature):
return web.Response(status=401)
return await self.client.get_updates(request) return await self.client.get_updates(request)