fix(feishu): validate verification token before reflecting url_verification challenge
When FEISHU_VERIFICATION_TOKEN is configured, an unauthenticated remote could previously prove endpoint control by sending a url_verification payload with any attacker-controlled challenge string — the handler reflected the challenge BEFORE running the token check. Move the verification_token check ahead of the url_verification echo so the challenge response is gated on a valid token. Add a regression test covering the wrong-token case. Also fix the stale test_connect_webhook_mode_starts_local_server fixture to set FEISHU_VERIFICATION_TOKEN (post #30746 webhook mode requires a secret). Salvaged from PR #29663 by @m0n3r0 — kept the url_verification reorder and its regression test; dropped the host-conditional weakening of the #30746 secret guard (we want webhook secrets required regardless of bind host, not only on 0.0.0.0/::). Docs updated to call out the gating. Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
@@ -3289,11 +3289,6 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||||||
self._record_webhook_anomaly(remote_ip, "400")
|
self._record_webhook_anomaly(remote_ip, "400")
|
||||||
return web.json_response({"code": 400, "msg": "invalid json"}, status=400)
|
return web.json_response({"code": 400, "msg": "invalid json"}, status=400)
|
||||||
|
|
||||||
# URL verification challenge — respond before other checks so that Feishu's
|
|
||||||
# subscription setup works even before encrypt_key is wired.
|
|
||||||
if payload.get("type") == "url_verification":
|
|
||||||
return web.json_response({"challenge": payload.get("challenge", "")})
|
|
||||||
|
|
||||||
# Verification token check — second layer of defence beyond signature (matches openclaw).
|
# Verification token check — second layer of defence beyond signature (matches openclaw).
|
||||||
if self._verification_token:
|
if self._verification_token:
|
||||||
header = payload.get("header") or {}
|
header = payload.get("header") or {}
|
||||||
@@ -3303,6 +3298,13 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||||||
self._record_webhook_anomaly(remote_ip, "401-token")
|
self._record_webhook_anomaly(remote_ip, "401-token")
|
||||||
return web.Response(status=401, text="Invalid verification token")
|
return web.Response(status=401, text="Invalid verification token")
|
||||||
|
|
||||||
|
# URL verification challenge — Feishu includes the verification token in
|
||||||
|
# challenge requests. Validate the token (above) before reflecting the
|
||||||
|
# challenge so an unauthenticated remote request cannot prove endpoint
|
||||||
|
# control by getting attacker-supplied challenge data echoed back.
|
||||||
|
if payload.get("type") == "url_verification":
|
||||||
|
return web.json_response({"challenge": payload.get("challenge", "")})
|
||||||
|
|
||||||
# Timing-safe signature verification (only enforced when encrypt_key is set).
|
# Timing-safe signature verification (only enforced when encrypt_key is set).
|
||||||
if self._encrypt_key and not self._is_webhook_signature_valid(request.headers, body_bytes):
|
if self._encrypt_key and not self._is_webhook_signature_valid(request.headers, body_bytes):
|
||||||
logger.warning("[Feishu] Webhook rejected: invalid signature from %s", remote_ip)
|
logger.warning("[Feishu] Webhook rejected: invalid signature from %s", remote_ip)
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase):
|
|||||||
"FEISHU_WEBHOOK_HOST": "127.0.0.1",
|
"FEISHU_WEBHOOK_HOST": "127.0.0.1",
|
||||||
"FEISHU_WEBHOOK_PORT": "9001",
|
"FEISHU_WEBHOOK_PORT": "9001",
|
||||||
"FEISHU_WEBHOOK_PATH": "/hook",
|
"FEISHU_WEBHOOK_PATH": "/hook",
|
||||||
|
"FEISHU_VERIFICATION_TOKEN": "vtok",
|
||||||
}, clear=True)
|
}, clear=True)
|
||||||
def test_connect_webhook_mode_starts_local_server(self):
|
def test_connect_webhook_mode_starts_local_server(self):
|
||||||
from gateway.config import PlatformConfig
|
from gateway.config import PlatformConfig
|
||||||
@@ -1538,6 +1539,34 @@ class TestAdapterBehavior(unittest.TestCase):
|
|||||||
self.assertEqual(response.status, 200)
|
self.assertEqual(response.status, 200)
|
||||||
adapter._on_message_event.assert_called_once()
|
adapter._on_message_event.assert_called_once()
|
||||||
|
|
||||||
|
@patch.dict(os.environ, {"FEISHU_VERIFICATION_TOKEN": "expected-token"}, clear=True)
|
||||||
|
def test_url_verification_requires_configured_verification_token(self):
|
||||||
|
"""url_verification must be rejected when token is set but mismatched.
|
||||||
|
|
||||||
|
Regression: previously the challenge was reflected before the token
|
||||||
|
check, so an unauthenticated remote could prove endpoint control by
|
||||||
|
sending an attacker-controlled challenge string.
|
||||||
|
"""
|
||||||
|
from gateway.config import PlatformConfig
|
||||||
|
from gateway.platforms.feishu import FeishuAdapter
|
||||||
|
|
||||||
|
adapter = FeishuAdapter(PlatformConfig())
|
||||||
|
body = json.dumps({
|
||||||
|
"type": "url_verification",
|
||||||
|
"token": "wrong-token",
|
||||||
|
"challenge": "attacker-controlled-challenge",
|
||||||
|
}).encode("utf-8")
|
||||||
|
request = SimpleNamespace(
|
||||||
|
remote="203.0.113.10",
|
||||||
|
content_length=None,
|
||||||
|
headers={},
|
||||||
|
read=AsyncMock(return_value=body),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||||
|
|
||||||
|
self.assertEqual(response.status, 401)
|
||||||
|
|
||||||
@patch.dict(os.environ, {}, clear=True)
|
@patch.dict(os.environ, {}, clear=True)
|
||||||
def test_process_inbound_message_uses_event_sender_identity_only(self):
|
def test_process_inbound_message_uses_event_sender_identity_only(self):
|
||||||
from gateway.config import PlatformConfig
|
from gateway.config import PlatformConfig
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ FEISHU_WEBHOOK_PORT=8765 # default: 8765
|
|||||||
FEISHU_WEBHOOK_PATH=/feishu/webhook # default: /feishu/webhook
|
FEISHU_WEBHOOK_PATH=/feishu/webhook # default: /feishu/webhook
|
||||||
```
|
```
|
||||||
|
|
||||||
When Feishu sends a URL verification challenge (`type: url_verification`), the webhook responds automatically so you can complete the subscription setup in the Feishu developer console.
|
When Feishu sends a URL verification challenge (`type: url_verification`), the webhook responds automatically so you can complete the subscription setup in the Feishu developer console. The challenge response is gated on `FEISHU_VERIFICATION_TOKEN` when set — challenge requests with a missing or mismatched token are rejected so an unauthenticated remote cannot prove endpoint control by echoing attacker-controlled challenge data.
|
||||||
|
|
||||||
## Step 3: Configure Hermes
|
## Step 3: Configure Hermes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user