From f378f00bfb8d40eb2e4a610ea17a856aea0c7088 Mon Sep 17 00:00:00 2001 From: m0n3r0 <34853915+m0n3r0@users.noreply.github.com> Date: Sun, 24 May 2026 04:50:51 -0700 Subject: [PATCH] fix(feishu): validate verification token before reflecting url_verification challenge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- gateway/platforms/feishu.py | 12 +++++---- tests/gateway/test_feishu.py | 29 +++++++++++++++++++++ website/docs/user-guide/messaging/feishu.md | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index b90dfcf86c7..2831476b5ba 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -3289,11 +3289,6 @@ class FeishuAdapter(BasePlatformAdapter): self._record_webhook_anomaly(remote_ip, "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). if self._verification_token: header = payload.get("header") or {} @@ -3303,6 +3298,13 @@ class FeishuAdapter(BasePlatformAdapter): self._record_webhook_anomaly(remote_ip, "401-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). 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) diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index ff6fcf56e3d..75f61923956 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -167,6 +167,7 @@ class TestFeishuAdapterMessaging(unittest.TestCase): "FEISHU_WEBHOOK_HOST": "127.0.0.1", "FEISHU_WEBHOOK_PORT": "9001", "FEISHU_WEBHOOK_PATH": "/hook", + "FEISHU_VERIFICATION_TOKEN": "vtok", }, clear=True) def test_connect_webhook_mode_starts_local_server(self): from gateway.config import PlatformConfig @@ -1538,6 +1539,34 @@ class TestAdapterBehavior(unittest.TestCase): self.assertEqual(response.status, 200) 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) def test_process_inbound_message_uses_event_sender_identity_only(self): from gateway.config import PlatformConfig diff --git a/website/docs/user-guide/messaging/feishu.md b/website/docs/user-guide/messaging/feishu.md index d5a84afc0e6..802f1d44f5a 100644 --- a/website/docs/user-guide/messaging/feishu.md +++ b/website/docs/user-guide/messaging/feishu.md @@ -93,7 +93,7 @@ FEISHU_WEBHOOK_PORT=8765 # default: 8765 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