fix(channels): reject unsigned webhook POSTs on encryption-configured channels (#401)
Closes #392 (uncontroversial part). WeChat (`_handle_message`) and Feishu (`_handle_event`) gated their signature/decryption checks behind a condition the REQUEST controls: - WeChat: `if encrypt and self._crypto:` -- a POST with no `<Encrypt>` element took the false branch and reached `_safe_process_message` without any verification, even when `encoding_aes_key` + `token` were configured. - Feishu: `if self.config.encrypt_key and "encrypt" in body:` -- a plaintext body skipped decryption entirely and was processed directly. Since the webhook port is the channel's only inbound boundary, an attacker could POST forged plaintext and reach the agent, spoofing `sender_id` / `FromUserName` (and, with an empty allowlist, passing the sender gate). Fix: when encryption is configured, an inbound POST MUST carry the encrypted field (`<Encrypt>` / `encrypt`) -- otherwise it is rejected with 403 and never reaches the agent. Plaintext mode (no encryption configured) is unchanged, so existing plaintext deployments are not affected. The remaining fail-closed question (what to do when credentials are entirely unset) is left for the maintainers to decide as the policy part of the issue. Regression tests (9 new): - WeChat: plaintext rejected / missing Encrypt rejected / bad signature rejected / valid signature decrypts and processes / plaintext still accepted when no crypto. - Feishu: plaintext rejected / non-dict body rejected / encrypted body decrypts and processes / plaintext still accepted when no encrypt_key. 93 tests in the two channel files pass; full suite 3045 passed, 13 skipped; ruff clean. Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
This commit is contained in:
@@ -337,9 +337,21 @@ class WeChatChannel(Channel, WebhookMixin, TokenMixin):
|
||||
logger.info(f"WeChat callback POST received, body length={len(body)}")
|
||||
xml_data = parse_xml(body)
|
||||
|
||||
# If encrypted, decrypt first
|
||||
# If encryption is configured, the inbound POST MUST carry an
|
||||
# <Encrypt> element and a matching msg_signature. An unsigned body
|
||||
# used to fall through to _safe_process_message and reach the agent
|
||||
# regardless of credentials, which made the encryption setup
|
||||
# ineffective (issue #392). Treat a missing <Encrypt> on an
|
||||
# encryption-configured channel as an authentication failure.
|
||||
encrypt = xml_data.get("Encrypt", "")
|
||||
if encrypt and self._crypto:
|
||||
if self._crypto:
|
||||
if not encrypt:
|
||||
logger.warning(
|
||||
"WeChat POST rejected: encryption is configured but the "
|
||||
"body has no <Encrypt> element (possible signature bypass)"
|
||||
)
|
||||
return web.Response(status=403)
|
||||
|
||||
signature = request.query.get("msg_signature", "")
|
||||
timestamp = request.query.get("timestamp", "")
|
||||
nonce = request.query.get("nonce", "")
|
||||
|
||||
Reference in New Issue
Block a user