From 8fd31eca66add8501620dcbf2a91601db5857c06 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 21:30:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(telegram):=20nonce=20UUID=20base64url=20?= =?UTF-8?q?=E5=A3=93=E7=B8=AE=20=E2=80=94=20=E5=BE=B9=E5=BA=95=E8=A7=A3?= =?UTF-8?q?=E6=B1=BA=20BUTTON=5FDATA=5FINVALID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前次修法(truncate random)不完整:host_restart_service(20 chars) 即使去掉 random 仍 68 bytes > 64 限制。 根本修法:UUID (36 chars) → base64url encode UUID bytes → 22 chars nonce 格式:{action}:{b64url_uuid}:{timestamp}:{random} 最長 case: host_restart_service(20)+22+10+8+3 colons = 63 bytes generate_callback_nonce: UUID → base64url 22 chars parse_callback_data: 22-char b64url → 還原完整 UUID,handler 不需改動 全 action 驗證:approve/silence/reject/docker_restart/host_restart_service/renew_cert 全部 ≤ 63 bytes,UUID round-trip 正確。 Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/services/security_interceptor.py | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/api/src/services/security_interceptor.py b/apps/api/src/services/security_interceptor.py index 2b7e1414..7d9cda7c 100644 --- a/apps/api/src/services/security_interceptor.py +++ b/apps/api/src/services/security_interceptor.py @@ -406,16 +406,22 @@ class TelegramSecurityInterceptor: """ import secrets + import base64, uuid as _uuid + timestamp = int(time.time()) random_part = secrets.token_hex(4) - nonce = f"{action}:{approval_id}:{timestamp}:{random_part}" + # UUID (36 chars) → base64url (22 chars) to keep nonce ≤ 63 bytes for all action names. + # Longest known action: host_restart_service (20) + 22 + ts(10) + rand(8) + 3 colons = 63 bytes. + try: + short_id = base64.urlsafe_b64encode( + _uuid.UUID(approval_id).bytes + ).rstrip(b"=").decode() + except (ValueError, AttributeError): + # Not a valid UUID (e.g. legacy format) — use as-is, may exceed limit but won't crash + short_id = approval_id - # Telegram callback_data limit is 64 bytes. - # Long action names (e.g. docker_restart=14 chars) with UUID approval_id push nonce to 71+ bytes. - # Drop the random suffix when over limit — timestamp still guarantees temporal uniqueness. - if len(nonce.encode()) > 63: - nonce = f"{action}:{approval_id}:{timestamp}" + nonce = f"{action}:{short_id}:{timestamp}:{random_part}" logger.debug( "callback_nonce_generated", @@ -454,15 +460,28 @@ class TelegramSecurityInterceptor: "is_info_action": True, } - # 接受 3-part(長 action 名截斷 random)和 4-part(標準)兩種格式 - if len(parts) not in (3, 4): + if len(parts) != 4: raise ValueError(f"Invalid callback_data format: {callback_data}") + import base64, uuid as _uuid + + raw_id = parts[1] + # Decode base64url-encoded UUID (22 chars) back to full UUID string. + # Legacy nonces with full UUID (36 chars) pass through unchanged. + if len(raw_id) == 22: + try: + decoded = _uuid.UUID(bytes=base64.urlsafe_b64decode(raw_id + "==")) + approval_id = str(decoded) + except Exception: + approval_id = raw_id + else: + approval_id = raw_id + return { "action": parts[0], - "approval_id": parts[1], + "approval_id": approval_id, "timestamp": int(parts[2]), - "nonce": callback_data, # 整個字串作為 nonce + "nonce": callback_data, "is_info_action": False, }