mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 01:15:24 +08:00
fix(lark): verify webhook event-subscription signature before dispatch
The Lark/Feishu webhook handler (both the current lark-omni adapter and the legacy lark adapter) parsed and dispatched inbound HTTP callback events without checking Lark's own X-Lark-Signature header, and the Encrypt Key config field used to decrypt payloads is optional and empty by default. Any caller who reached a bot's webhook URL could inject fabricated message/card-action events with an attacker-chosen sender, chat, and content. Add signature verification matching Lark's documented event-security-verification scheme (sha256 of timestamp + nonce + encrypt_key + body, compared to X-Lark-Signature) to both implementations, rejecting any callback that lacks a valid signature or whose bot has no Encrypt Key configured. Adds regression tests covering the rejection paths and updates the two existing webhook tests to present a valid signature.
This commit is contained in:
@@ -9,6 +9,7 @@ import threading
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
@@ -629,9 +630,34 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
return f'{event.group.id}_{thread_id}'
|
||||
return None
|
||||
|
||||
def _verify_webhook_signature(self, request, raw_body: bytes) -> bool:
|
||||
"""Verify the Lark/Feishu event-subscription callback signature.
|
||||
|
||||
Per Lark's own event-security-verification protocol, a callback is
|
||||
authentic only if its ``X-Lark-Signature`` header equals
|
||||
sha256(timestamp + nonce + encrypt_key + body). Without this check
|
||||
any caller who reaches this webhook path can inject fabricated
|
||||
events (spoofed messages, card actions) regardless of whether they
|
||||
hold the app's Encrypt Key.
|
||||
"""
|
||||
encrypt_key = self.config.get('encrypt-key', '') or ''
|
||||
if not encrypt_key:
|
||||
return False
|
||||
timestamp = request.headers.get('X-Lark-Request-Timestamp', '')
|
||||
nonce = request.headers.get('X-Lark-Request-Nonce', '')
|
||||
signature = request.headers.get('X-Lark-Signature', '')
|
||||
if not timestamp or not nonce or not signature:
|
||||
return False
|
||||
expected = hashlib.sha256((timestamp + nonce + encrypt_key).encode('utf-8') + raw_body).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
||||
try:
|
||||
data = await request.json
|
||||
raw_body = await request.get_data()
|
||||
if not self._verify_webhook_signature(request, raw_body):
|
||||
await self.logger.error('Lark webhook signature verification failed, rejecting callback')
|
||||
return {'code': 403, 'message': 'signature verification failed'}, 403
|
||||
data = json.loads(raw_body)
|
||||
if 'encrypt' in data:
|
||||
data = json.loads(self.cipher.decrypt_string(data['encrypt']))
|
||||
event_type = self.get_event_type(data)
|
||||
|
||||
@@ -5,6 +5,7 @@ from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody, Crea
|
||||
import traceback
|
||||
import typing
|
||||
import asyncio
|
||||
import hmac
|
||||
import re
|
||||
import base64
|
||||
import uuid
|
||||
@@ -2973,6 +2974,27 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
else:
|
||||
return data['type']
|
||||
|
||||
def _verify_webhook_signature(self, request, raw_body: bytes) -> bool:
|
||||
"""Verify the Lark/Feishu event-subscription callback signature.
|
||||
|
||||
Per Lark's event-security-verification protocol, a callback is
|
||||
authentic only if its ``X-Lark-Signature`` header equals
|
||||
sha256(timestamp + nonce + encrypt_key + body). Without this check
|
||||
any caller who reaches this webhook path can inject fabricated
|
||||
events (spoofed messages, card actions) regardless of whether they
|
||||
hold the app's Encrypt Key.
|
||||
"""
|
||||
encrypt_key = self.config.get('encrypt-key', '') or ''
|
||||
if not encrypt_key:
|
||||
return False
|
||||
timestamp = request.headers.get('X-Lark-Request-Timestamp', '')
|
||||
nonce = request.headers.get('X-Lark-Request-Nonce', '')
|
||||
signature = request.headers.get('X-Lark-Signature', '')
|
||||
if not timestamp or not nonce or not signature:
|
||||
return False
|
||||
expected = hashlib.sha256((timestamp + nonce + encrypt_key).encode('utf-8') + raw_body).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
||||
"""处理统一 webhook 请求。
|
||||
Args:
|
||||
@@ -2983,7 +3005,11 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
响应数据
|
||||
"""
|
||||
try:
|
||||
data = await request.json
|
||||
raw_body = await request.get_data()
|
||||
if not self._verify_webhook_signature(request, raw_body):
|
||||
await self.logger.error('Lark webhook signature verification failed, rejecting callback')
|
||||
return {'code': 403, 'message': 'signature verification failed'}, 403
|
||||
data = json.loads(raw_body)
|
||||
|
||||
if 'encrypt' in data:
|
||||
encrypted = data['encrypt']
|
||||
@@ -2991,7 +3017,6 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
type = self.get_event_type(data)
|
||||
context = EventContext(data)
|
||||
if 'url_verification' == type:
|
||||
# todo 验证verification token
|
||||
return {'challenge': data.get('challenge')}
|
||||
elif 'app_ticket' == type:
|
||||
self.app_ticket = context.event['app_ticket']
|
||||
|
||||
Reference in New Issue
Block a user