Skip to content

fix(weixin): confirm the WEBP signature, not just the RIFF prefix#5285

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
he-yufeng:fix/weixin-webp-signature
Jun 18, 2026
Merged

fix(weixin): confirm the WEBP signature, not just the RIFF prefix#5285
wenshao merged 1 commit into
QwenLM:mainfrom
he-yufeng:fix/weixin-webp-signature

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

What this PR does

detectImageMime in the WeChat channel now confirms the WEBP marker at bytes 8-11 before classifying data as image/webp, instead of accepting any input that merely begins with the four RIFF bytes.

Why it's needed

detectImageMime is the magic-byte check inside validateImagePath, which exists to confirm that an image file's real content matches its claimed extension before it is uploaded. RIFF is a generic container also used by WAV and AVI, so the RIFF prefix alone does not prove the data is WebP: a non-WebP RIFF file (for example a WAV renamed to .webp) previously returned image/webp and so passed the verification. This is the same incomplete-signature problem #2970 fixed for the PNG branch, and core's imageTokenizer already validates both RIFF and WEBP, so this brings the weixin channel in line.

Reviewer Test Plan

How to verify

A new unit test feeds a RIFF....WAVE buffer and asserts detectImageMime throws Unrecognized image format; on main it wrongly returns image/webp. The existing WebP test was updated to use a full RIFF....WEBP header (12 bytes) rather than the 4-byte RIFF prefix.

npx vitest run packages/channels/weixin/src/send.test.ts -t detectImageMime

Expected: the detectImageMime suite passes (6 tests); the new WAV case fails on main and passes with this change.

Evidence (Before & After)

N/A — non-user-visible parsing fix covered by unit tests.

Tested on

OS Status
🍏 macOS N/A
🪟 Windows
🐧 Linux ⚠️

Note: three failures in validateImagePath/sendImage already reproduce on an unmodified main on Windows (realpath/backslash mocking) and are unrelated to this change; the detectImageMime suite is green.

Environment (optional)

N/A — unit tests only.

Risk & Scope

  • Main risk or tradeoff: a "WebP" file whose header is shorter than 12 bytes would no longer be detected, but the caller reads 16 bytes and every valid WebP carries the 12-byte RIFF....WEBP header, so rejecting shorter input is correct.
  • Not validated / out of scope: only the weixin channel's detectImageMime is touched; other channels and image paths are unchanged.
  • Breaking changes / migration notes: none.

Linked Issues

None.

detectImageMime treated any data starting with the four RIFF bytes as
image/webp, but RIFF is a generic container also used by WAV and AVI. A
non-WebP RIFF file (e.g. a WAV renamed to .webp) therefore passed the
magic-byte check in validateImagePath, which exists to confirm a file's real
type. Verify the 'WEBP' marker at bytes 8-11 as well, matching the stricter
check already in core's imageTokenizer and the earlier full-signature PNG fix.
data[1] === 0x49 &&
data[2] === 0x46 &&
data[3] === 0x46
data[3] === 0x46 &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The WebP check accesses data[8] through data[11] without an explicit length guard. This works in practice — Buffer[i] returns undefined for out-of-bounds indices, and undefined === 0x57 is false, so short buffers fall through safely. However, this relies on implicit JS behavior rather than an explicit check. A future refactor (e.g., switching to data.readUInt32BE(8)) could silently break this.

Consider adding an explicit guard:

Suggested change
data[3] === 0x46 &&
if (
data.length >= 12 &&
data[0] === 0x52 &&
data[1] === 0x49 &&
data[2] === 0x46 &&
data[3] === 0x46 &&
data[8] === 0x57 &&
data[9] === 0x45 &&
data[10] === 0x42 &&
data[11] === 0x50

— qwen3.7-max via Qwen Code /review

0x45,
0x42,
0x50, // "WEBP"
]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The old test used a 4-byte RIFF buffer which implicitly covered the short-buffer path. The replacement uses a full 12-byte header, so there's no longer a test asserting that a truncated RIFF buffer (< 12 bytes) falls through gracefully rather than crashing. Consider adding:

it('does not crash on a truncated RIFF buffer (< 12 bytes)', () => {
  const buf = Buffer.from([0x52, 0x49, 0x46, 0x46]); // only 4 bytes
  expect(() => detectImageMime(buf)).toThrow('Unrecognized image format');
});

This locks in the graceful fall-through behavior so future refactors cannot silently break it.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: solid correctness fix. detectImageMime currently accepts any RIFF container as WebP — a WAV or AVI file renamed to .webp would pass image validation. This is the same incomplete-signature issue that was previously fixed for PNG, and core's imageTokenizer already validates both RIFF and WEBP markers. Bringing the weixin channel in line is clearly the right call.

On approach: scope is tight — two files, minimal diff. The implementation adds the four-byte WEBP check at offset 8-11, mirrors core's approach exactly, and the test coverage is good (WAV misidentification test + strengthened existing WebP test to use a proper 12-byte header). Nothing to cut.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个明确的正确性修复。detectImageMime 目前将任何 RIFF 容器都识别为 WebP——重命名为 .webp 的 WAV 或 AVI 文件会通过图片验证。这与之前 PNG 分支修复的不完整签名问题相同,而 core 的 imageTokenizer 已经同时验证 RIFF 和 WEBP 标记。将 weixin 频道对齐是正确的做法。

方案:范围紧凑——两个文件,最小化 diff。实现在偏移 8-11 处添加了四字节 WEBP 校验,与 core 的做法完全一致,测试覆盖良好(WAV 误识别测试 + 将现有 WebP 测试加强为使用完整的 12 字节头部)。无需删减。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent baseline: I'd add the WEBP marker check at offset 8-11, matching how core's imageTokenizer already does it. One concern — core uses buffer.subarray(8, 12).toString('ascii'), but this function uses individual byte comparisons throughout. The PR correctly follows the local convention (byte-by-byte), which is more consistent and avoids the subarray allocation.

Reviewed the diff — no correctness issues, no security concerns, no AGENTS.md violations. The implementation handles short buffers correctly (JavaScript returns undefined for out-of-bounds array access, so undefined !== 0x57 fails the check, which is the right outcome).

Code is clean. No blockers.

Test Results

On main (baseline — bug present)

 ❯ src/send.test.ts (34 tests | 1 failed | 28 skipped) 9ms
   ✓ detectImageMime > detects PNG magic bytes
   ✓ detectImageMime > detects GIF magic bytes
   ✓ detectImageMime > detects WebP magic bytes (RIFF....WEBP)
   × detectImageMime > does not misidentify a non-WebP RIFF container (e.g. WAV) as WebP
     → AssertionError: expected [Function] to throw an error
   ✓ detectImageMime > detects JPEG magic bytes
   ✓ detectImageMime > throws for unrecognized magic bytes

 Test Files  1 failed (1)
      Tests  1 failed | 5 passed | 28 skipped (34)

The WAV test fails as expected — on main, detectImageMime returns image/webp for a WAV buffer because it only checks the RIFF prefix.

On PR branch (fix applied)

 ✓ src/send.test.ts (34 tests | 28 skipped) 3ms

 Test Files  1 passed (1)
      Tests  6 passed | 28 skipped (34)
   Duration  288ms

All 6 detectImageMime tests pass. Bug confirmed, fix verified.

Note: this is a non-user-visible parsing fix covered by unit tests — tmux real-scenario testing is N/A.

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Straightforward call. The PR is a textbook minimal fix: one function, four extra byte comparisons, mirrors core's existing approach, and the test suite proves the bug on main and the fix on the PR branch. No scope creep, no drive-by refactors. The kind of PR you wish all bug fixes looked like.

Approving. ✅

中文说明

判断很直接。这个 PR 是一个教科书级别的最小修复:一个函数,四个额外的字节比较,与 core 现有做法一致,测试在 main 上证实了 bug,在 PR 分支上验证了修复。没有范围蔓延,没有顺手重构。希望所有 bug 修复都长这样。

批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — built-artifact end-to-end A/B + tests + mutation

Verified on a clean build of the PR head (a511598) in an isolated worktree. Verdict: correct security hardening, no regression — recommend merge. This is a non‑TUI channel fix, so the "real test" is the shipped dist function run end‑to‑end against real on‑disk files (in a real tmux/shell session), not a TUI A/B.

What the change does

detectImageMime now requires the WEBP fourcc at bytes 8‑11 before returning image/webp, instead of accepting anything that merely starts with the 4 RIFF bytes.

Root cause (security relevance)

detectImageMime is the magic‑byte gate inside validateImagePath, which confirms a file's real content matches its claimed extension before it is uploaded to the WeChat CDN. RIFF is a generic container shared by WAV/AVI, so the prefix alone doesn't prove WebP — a non‑WebP RIFF file (e.g. a WAV renamed .webp) previously passed verification. Same incomplete‑signature class as the PNG fix in #2970; brings weixin in line with core's imageTokenizer (which already checks both RIFF and WEBP).

Evidence

Check Result
Build real package (npm ci + build) ✅ exit 0
Declared test — detectImageMime suite ✅ 6 passed
Mutation test (deterministic) ✅ revert send.ts→base → new WAV case fails (expected [Function] to throw)
End‑to‑end A/B on built dist + real files ✅ see below

End‑to‑end A/B — I ran the built dist/send.js detectImageMime/validateImagePath against real files: a genuine WAV from afconvert renamed .webp, a real 12‑byte lossless WebP, and real PNG/JPEG/GIF from sips. Only core/dist… (here only weixin/dist) differs between the two runs — fixed vs rebuilt‑base. The pre‑fix dist was byte‑checked to confirm it carried the RIFF‑only logic before the run.

Input PRE‑FIX (base) FIXED (this PR)
RIFF…WAVE buffer (a WAV) image/webp throws Unrecognized image format
RIFF only, 4 bytes image/webp throws
real WAV renamed .webpvalidateImagePath ACCEPT ❌ (would be uploaded) REJECT
real WebP / PNG / JPEG / GIF → validateImagePath ACCEPT ✅ ACCEPT ✅ (no regression)
PRE-FIX (real tmux run):                         FIXED:
❌ RIFF....WAVE (WAV)  -> image/webp              ✅ RIFF....WAVE (WAV)  -> THROW(Unrecognized…)
❌ RIFF only (4 bytes) -> image/webp              ✅ RIFF only (4 bytes) -> THROW(Unrecognized…)
❌ wav_renamed.webp    ACCEPT -> /…/wav_renamed   ✅ wav_renamed.webp    REJECT: Unrecognized image format
✅ tiny.{png,jpg,gif,webp} ACCEPT                 ✅ tiny.{png,jpg,gif,webp} ACCEPT
=== 3 MISMATCH(es) ===                            === ALL EXPECTATIONS MET ===

Correctness notes

  • A "WebP" header shorter than 12 bytes is now rejected — correct: the WEBP fourcc at offset 8 is mandatory in the WebP container, validateImagePath reads 16 bytes, so every valid WebP qualifies. (Confirmed: a 4‑byte RIFF throws.)
  • PNG / GIF / JPEG branches are untouched — all real samples still classify correctly, so no legitimate upload regresses.
  • 4 extra byte comparisons; negligible.

Verdict

A small, well‑reasoned security fix (blocks content‑type spoofing of WeChat image uploads), fully covered by a non‑vacuous regression test, and confirmed end‑to‑end against the real built artifact + real files. Recommend merge.

🇨🇳 中文版(点击展开)

✅ 维护者验证 —— 构建产物端到端 A/B + 测试 + 变异测试

在隔离 worktree 中对 PR head(a511598)全新构建后验证。结论:正确的安全加固、无回归 —— 建议合并。 这是个非 TUI 的 channel 修复,所以"真实测试"是用构建产物 dist 里的函数对真实磁盘文件做端到端运行(在真实 tmux/shell 会话里),而非 TUI A/B。

改动做了什么

detectImageMime 现在要求 字节 8‑11WEBP fourcc 才返回 image/webp,而不再是"只要以 4 个 RIFF 字节开头"就接受。

根因(安全相关)

detectImageMimevalidateImagePath 里的魔数关卡,用于在文件上传到微信 CDN 之前确认文件真实内容与其声称的扩展名一致。RIFF 是 WAV/AVI 共用的通用容器,所以仅凭前缀不能证明是 WebP —— 一个非 WebP 的 RIFF 文件(例如 WAV 改名 .webp)以前能通过校验。与 #2970 修复 PNG 的"签名不完整"问题同类;让 weixin 与 core 的 imageTokenizer(已同时校验 RIFFWEBP)保持一致。

证据

检查项 结果
构建真实包(npm ci + build) ✅ exit 0
声明测试 —— detectImageMime 套件 ✅ 6 通过
变异测试(确定性) ✅ 还原 send.ts→base → 新增 WAV 用例失败expected [Function] to throw
端到端 A/B(构建产物 dist + 真实文件) ✅ 见下

端到端 A/B —— 我用构建产物 dist/send.jsdetectImageMime/validateImagePath真实文件afconvert 生成的真 WAV 改名 .webp、一个真实 12 字节 lossless WebP、sips 生成的真 PNG/JPEG/GIF。两次运行只有 weixin/dist 不同(修复版 vs 重建的 base 版),且运行前已逐字节确认 pre‑fix dist 确实是 RIFF‑only 逻辑。

输入 PRE‑FIX(base) FIXED(本 PR)
RIFF…WAVE buffer(WAV) image/webp 抛错 Unrecognized image format
RIFF 4 字节 image/webp 抛错
真 WAV 改名 .webpvalidateImagePath ACCEPT ❌(会被上传) REJECT
真 WebP / PNG / JPEG / GIF → validateImagePath ACCEPT ✅ ACCEPT ✅(无回归)
PRE-FIX(真实 tmux 运行):                         FIXED:
❌ RIFF....WAVE (WAV)  -> image/webp              ✅ RIFF....WAVE (WAV)  -> THROW(Unrecognized…)
❌ RIFF only (4 bytes) -> image/webp              ✅ RIFF only (4 bytes) -> THROW(Unrecognized…)
❌ wav_renamed.webp    ACCEPT -> /…/wav_renamed   ✅ wav_renamed.webp    REJECT: Unrecognized image format
✅ tiny.{png,jpg,gif,webp} ACCEPT                 ✅ tiny.{png,jpg,gif,webp} ACCEPT
=== 3 MISMATCH(es) ===                            === ALL EXPECTATIONS MET ===

正确性说明

  • 短于 12 字节的"WebP"头现在会被拒绝 —— 正确:WebP 容器规范要求 offset 8 处必须是 WEBP fourcc,validateImagePath 会读 16 字节,所以任何合法 WebP 都满足。(已确认:4 字节 RIFF 会抛错。)
  • PNG / GIF / JPEG 分支未改动 —— 所有真实样本仍正确分类,没有任何合法上传被回归。
  • 多 4 次字节比较;可忽略。

结论

一个小而论证充分的安全修复(阻断微信图片上传的内容类型伪造),有非空过场的回归测试完整覆盖,并用真实构建产物 + 真实文件端到端确认。建议合并。

Verification method: worktree build + declared unit test + source‑revert mutation test + end‑to‑end harness importing the built dist/send.js against real afconvert/sips‑generated files (fixed vs rebuilt‑base weixin/dist).

@wenshao
wenshao merged commit 5a84ba0 into QwenLM:main Jun 18, 2026
40 checks passed
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.

3 participants