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, }