diff --git a/quantara/web_app/telegram/utils.py b/quantara/web_app/telegram/utils.py index 51b2e6cb..93355d08 100644 --- a/quantara/web_app/telegram/utils.py +++ b/quantara/web_app/telegram/utils.py @@ -53,7 +53,13 @@ def check_telegram_authorization( secret_key, data_check_string.encode(), hashlib.sha256 ).hexdigest() - if hash_value != check_hash: + # Constant-time compare so a remote timing oracle cannot recover the + # expected hash byte-by-byte (see issue #203). Unequal-length inputs + # make compare_digest raise; treat that as a failed verification. + try: + if not hmac.compare_digest(hash_value, check_hash): + return False + except (TypeError, ValueError): return False if expiration_seconds and (int(time.time()) - int(auth_data.get("auth_date", 0))) > expiration_seconds: diff --git a/quantara/web_app/tests/test_telegram.py b/quantara/web_app/tests/test_telegram.py index 685d5462..707b883a 100644 --- a/quantara/web_app/tests/test_telegram.py +++ b/quantara/web_app/tests/test_telegram.py @@ -146,6 +146,22 @@ def test_rejects_expired_payload(self): ) assert ok is False + def test_uses_constant_time_compare(self): + """Hash comparison must use hmac.compare_digest (timing-safe).""" + import hmac as hmac_mod + from web_app.telegram.utils import check_telegram_authorization + + data = self._build_auth(self.BOT_TOKEN) + with patch.object( + hmac_mod, "compare_digest", wraps=hmac_mod.compare_digest + ) as mock_cmp: + assert check_telegram_authorization(self.BOT_TOKEN, data) is True + assert mock_cmp.called + # Both sides must be str hex digests of equal length. + left, right = mock_cmp.call_args[0] + assert isinstance(left, str) and isinstance(right, str) + assert len(left) == len(right) == 64 + # ───────────────────────────────────────────────────────────────────────────── # 2. notifications.send_health_ratio_notification