Skip to content

fix(weixin): Batch-5 polish — SSRF allowlist, qrcode dep, session retry, macOS SSL, signature alignment#11634

Merged
teknium1 merged 6 commits into
mainfrom
hermes/hermes-7078c790
Apr 17, 2026
Merged

fix(weixin): Batch-5 polish — SSRF allowlist, qrcode dep, session retry, macOS SSL, signature alignment#11634
teknium1 merged 6 commits into
mainfrom
hermes/hermes-7078c790

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Batch-5 Weixin polish — five contributor PRs salvaged together. Security, packaging, resilience, and platform-compat fixes.

Included contributor work

#9222 @memosrfix(security): validate WeChat media URLs against CDN allowlist to prevent SSRF
Commit 95e15e10. Adds _WEIXIN_CDN_ALLOWLIST frozenset (7 known WeChat CDN hosts) and _assert_weixin_cdn_url() validation before any media download via full_url. Complements the existing is_safe_url() check on _download_remote_media — defense in depth. Rejects file://, non-WeChat hosts, and unparseable URLs.

#9504 @anthhubfix(packaging): include qrcode in messaging extra
Commit 877674e3. Adds qrcode>=7.0,<8 to project.optional-dependencies.messaging so hermes-agent[messaging] can render terminal QR codes for Weixin setup without a follow-up pip install qrcode. Adds metadata regression test. Updates weixin docs. Closes #9431.

#9928 @jinzheng8115fix(weixin): retry send without context_token on iLink session expiry
Commit 942086d8. When _send_text_chunk detects an iLink errcode: -14 (session expired), strips the stale context_token, clears it from ContextTokenStore, and retries once. Fixes silent drop of cron-initiated pushes (weather reports, digests, etc.) after overnight idle. Also makes _send_message return the API response dict so callers can inspect errcode.

#8730 @shenuufix(weixin): macOS SSL cert, QR data, and refresh rendering
Commit c1b0efbb. Three fixes:

  1. Use certifi CA bundle for SSL verification — Homebrew Python on macOS Apple Silicon can't verify ilinkai.weixin.qq.com against the default cert store. Refactored into _make_ssl_connector() helper with graceful fallback when certifi is unavailable.
  2. Prefer qrcode_img_content (liteapp URL) over qrcode (hex token) when encoding the QR ASCII — only the URL is scannable by WeChat.
  3. Re-render QR ASCII on the refresh path, not just print the URL.

#10342 @xiayh0107Fix Weixin media uploads and refresh lockfile
Commit d8ab47f9. Aligns send_document() signature with BasePlatformAdapter — adds file_name, reply_to params and **kwargs for forward-compat. Tightens send_image_file() body to use keyword args. Adds regression tests covering keyword-arg acceptance and the upload_full_url POST behavior with hex-encoded AES key.

Note: Much of #10342's original scope (the send_image_file parameter rename, MEDIA extraction, voice fallback, markdown preservation) was already landed via Batches 1/3. What survived here is the send_document base-class alignment + the upload_full_url test, which weren't covered elsewhere.

Conflict resolutions

Five resolution points (all mechanical, all verified):

  1. _send_text_chunk() in weixin.pyfix(weixin): retry send without context_token on iLink session expiry #9928's resp = await _send_message(self._session, ...) vs current await _send_message(self._send_session, ...). Kept PR's return capture + HEAD's Batch-4 session name: resp = await _send_message(self._send_session, ...).

  2. SSL connector in qr_login() / connect() / send_weixin_direct()fix(weixin): macOS SSL cert verification, QR scan data, and refresh rendering #8730's aiohttp.ClientSession(connector=certifi_ctx) vs HEAD's aiohttp.ClientSession(trust_env=True) (would have silently regressed proxy env var handling). Refactored into a module-level _make_ssl_connector() helper used at all 4 ClientSession creation sites with trust_env=True preserved:

    aiohttp.ClientSession(trust_env=True, connector=_make_ssl_connector())

    Helper gracefully returns None if certifi is unavailable (aiohttp then uses default ctx, still honors SSL_CERT_FILE via trust_env).

  3. send_image_file() bodyFix Weixin media uploads #10342 rewrote with keyword args + del reply_to, kwargs. Accepted PR's more thorough version.

  4. send_document() bodyFix Weixin media uploads #10342's self._session check vs HEAD's self._send_session. Kept HEAD's Batch-4 split + added PR's del file_name, reply_to, metadata, kwargs.

  5. Test fixture alignment — Batch-1's TestWeixinSendImageFileParameterName asserted the OLD positional call signature; Fix Weixin media uploads #10342 changed send_image_file to call send_document(chat_id=chat_id, ...) with keyword args and caption=None default. Updated tests to match (chat_id as keyword, caption=None when not passed). Fix Weixin media uploads #10342's TestWeixinOutboundMedia mocked only _session but code uses _send_session after Batch-4 — added _send_session = session companion.

Verification

  • Full targeted suite: 435 passed across test_weixin + test_qqbot + test_unauthorized_dm_behavior + test_platform_base + test_send_image_file + test_send_retry + cron/test_scheduler + test_send_message_tool + test_url_safety + test_project_metadata.
  • Integration sanity (runtime, real imports):
    • _WEIXIN_CDN_ALLOWLIST present, contains 7 WeChat CDN hosts
    • _assert_weixin_cdn_url() blocks evil.com, blocks file:// scheme, passes legitimate novac2c.cdn.weixin.qq.com
    • _make_ssl_connector() returns a TCPConnector in async context (graceful None fallback sync-calls tested separately — the helper is only called from async contexts in production)
    • SESSION_EXPIRED_ERRCODE = -14 present
    • _send_message return annotation updated to Dict[str, Any]
    • send_document signature now matches base class: chat_id, file_path, caption, file_name, reply_to, metadata, **kwargs
  • py_compile OK on all changed files.

AUTHOR_MAP additions

  • memosr_email@gmail.com → memosr
  • anthhub@163.com → anthhub
  • shenuu@gmail.com → shenuu
  • xiayh17@gmail.com → xiayh0107
  • jinzheng8115 auto-resolves via noreply pattern

On merge

Will rebase-merge to preserve per-commit authorship across 5 contributors, then close #9222, #9504, #9928, #8730, #10342 with credit pointing here.

This closes out the Weixin/QQBot PR queue after Batches 1-5.

memosr and others added 6 commits April 17, 2026 06:37
iLink context_token has a limited TTL. When no user message has arrived
for an extended period (e.g. overnight), cron-initiated pushes fail with
errcode -14 (session timeout).

Tested that iLink accepts sends without context_token as a degraded
fallback, so we now automatically strip the expired token and retry
once. This keeps scheduled push messages (weather, digests, etc.)
working reliably without requiring a user message to refresh the
session first.

Changes:
- _send_text_chunk() catches iLinkDeliveryError with session-expired
  errcode (-14) and retries without context_token
- Stale tokens are cleared from ContextTokenStore on session expiry
- All 34 existing weixin tests pass
- Use certifi CA bundle for aiohttp SSL in qr_login(), start(), and
  send_weixin_direct() to fix SSL verification failures against
  Tencent's iLink server on macOS (Homebrew OpenSSL lacks system certs)
- Fix QR code data: encode qrcode_img_content (full liteapp URL) instead
  of raw hex token — WeChat needs the full URL to resolve the scan
- Render ASCII QR on refresh so the user can re-scan without restarting
- Improve error message on QR render failure to show the actual exception

Tested on macOS (Apple Silicon, Homebrew Python 3.13)
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Supply Chain Risk Detected

This PR contains patterns commonly associated with supply chain attacks. This does not mean the PR is malicious — but these patterns require careful human review before merging.

⚠️ WARNING: base64 encoding/decoding detected

Base64 has legitimate uses (images, JWT, etc.) but is also commonly used to obfuscate malicious payloads. Verify the usage is appropriate.

Matches (first 20):

425:+        expected_aes_key = base64.b64encode(aes_key.hex().encode("ascii")).decode("ascii")

⚠️ WARNING: Dependency manifest files modified

Changes to dependency files can introduce new packages or change version pins. Verify all dependency changes are intentional and from trusted sources.

Files:

pyproject.toml

Automated scan triggered by supply-chain-audit. If this is a false positive, a maintainer can approve after manual review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add qrcode to messaging optional dependencies for Weixin setup

4 participants