fix(wecom): constant-time callback signature comparison - #43937
Conversation
The WeCom callback crypto verified inbound message signatures with a plain `expected != msg_signature` string comparison. Python short- circuits string equality on the first differing byte, so the response time leaks how many leading characters matched. A network attacker who controls the timestamp/nonce/echostr of a callback can recover a valid `msg_signature` byte-by-byte and forge authenticated WeCom callbacks (spoofed inbound messages / URL verification). Use `hmac.compare_digest` for a constant-time comparison, matching the pattern already used by the Feishu, LINE, and webhook verifiers. Also coerce a missing signature to '' so a None value surfaces as a clean SignatureError instead of a TypeError. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Verification review — reviewed the constant-time signature comparison fix. The change is correct and minimal:
Clean security fix. |
|
Verification review — reviewed the diff for constant-time signature comparison quality. The fix is clean and correct:
This is a minimal, well-scoped security hardening — one line of production code changed, three focused tests added. No behavioral regressions expected. |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Security: WeCom constant-time callback signature comparison
- Timing attack fix: replaces
expected != msg_signaturewithhmac.compare_digest(expected, msg_signature or ""). A plain!=leaks how many leading characters matched via response timing, allowing a network attacker to forge a valid signature byte-by-byte. - None guard:
msg_signature or ""preventsTypeErrorwhen the signature field is missing/None, converting it to a guaranteed mismatch. - Tests: spy test confirms
hmac.compare_digestis called, plus a test for the None signature case.
Reviewed by Hermes Agent
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused security hardening. The premise remains valid on current main, but the PR must be relocated before it can be salvaged.
Problems
- The changed production file,
gateway/platforms/wecom_crypto.py, was renamed by5600105478ffde29d7566b45421b100eaa29c4eftoplugins/platforms/wecom/wecom_crypto.py. The active verifier still usesexpected != msg_signatureatplugins/platforms/wecom/wecom_crypto.py:90, so the proposed edit would not reach the live code. - The added spy test imports
gateway.platforms.wecom_crypto; current tests import the plugin module attests/gateway/test_wecom_callback.py:10.
Suggested changes
- Move the
hmac.compare_digest(expected, msg_signature or "")change toplugins/platforms/wecom/wecom_crypto.py:90. - Update the new test import to
plugins.platforms.wecom.wecom_crypto.
Automated hermes-sweeper review.
| def decrypt(self, msg_signature: str, timestamp: str, nonce: str, encrypt: str) -> bytes: | ||
| expected = _sha1_signature(self.token, timestamp, nonce, encrypt) | ||
| if expected != msg_signature: | ||
| # Constant-time comparison: a plain ``!=`` leaks how many leading |
There was a problem hiding this comment.
This source file was renamed to plugins/platforms/wecom/wecom_crypto.py by 5600105478ffde29d7566b45421b100eaa29c4ef. Please transplant this change to the active plugin file; its live comparison remains at line 90.
|
suggesting changes The timing-safe comparison is the right direction, but it compares Python Security evidence:
Not checked:
Signed: GPT-5.6-sol-xhigh in Codex |
Summary
WeCom callback signature verification (
gateway/platforms/wecom_crypto.py) compared the expected SHA1 signature with the attacker-suppliedmsg_signatureusing a plain!=string comparison.python expected = _sha1_signature(self.token, timestamp, nonce, encrypt) if expected != msg_signature: raise SignatureError("signature mismatch")Python's
str.__ne__short-circuits on the first differing byte, so the verification time is proportional to the number of leading characters that matched. This is a classic timing side-channel: a network attacker who controlstimestamp/nonce/echostrin a callback request can measure response latency and recover a validmsg_signaturebyte-by-byte, then forge authenticated WeCom callbacks (spoofed inbound messages, URL-verification bypass).Fix
hmac.compare_digest(constant-time), matching the existing pattern in the Feishu (feishu.py), LINE (line.py), and webhook (webhook.py) verifiers.""so aNonevalue raises a cleanSignatureErrorinstead of aTypeError.No behavior change for legitimate callers — valid signatures still pass, invalid ones still raise
SignatureError.Tests
tests/gateway/test_wecom_callback.py:hmac.compare_digestNonesignature raisesSignatureError(notTypeError).venv/Scripts/python -m pytest tests/gateway/test_wecom_callback.py→ 14 passed.Scope
Upstream-only file, no fork-specific code. Distinct from the open WeCom PRs (media attachments / target-ref parsing), which do not touch the crypto verifier.
Made with Cursor