Skip to content

fix(whatsapp): canonicalize outbound media delivery#69813

Merged
mcaxtr merged 6 commits into
mainfrom
refactor/whatsapp-canonical-outbound-media
Apr 24, 2026
Merged

fix(whatsapp): canonicalize outbound media delivery#69813
mcaxtr merged 6 commits into
mainfrom
refactor/whatsapp-canonical-outbound-media

Conversation

@mcaxtr

@mcaxtr mcaxtr commented Apr 21, 2026

Copy link
Copy Markdown
Member

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: WhatsApp outbound media behavior had split ownership across extensions/whatsapp/src/channel-outbound.ts, extensions/whatsapp/src/outbound-adapter.ts, extensions/whatsapp/src/send.ts, and extensions/whatsapp/src/auto-reply/deliver-reply.ts, so media fixes kept landing in one path while another path still drifted.
  • Why it matters: gateway/channel sends, payload sends, and auto-reply sends could disagree on media detection, source selection, caption placement, retry behavior, and fallback behavior, causing dropped media, text downgrades, duplicate sends, or inconsistent delivery.
  • What changed in WhatsApp: introduced a canonical plugin-local outbound media contract in extensions/whatsapp/src/outbound-media-contract.ts and routed the outbound paths through it so media normalization, caption-first behavior, retry classification, logging shape, and listener/send resolution stay aligned.
  • What changed in the reply pipeline: preserved final assistant media directives through embedded-runner payload construction, made non-streaming captioned media use the final payload path once, kept media-only orphan blocks deliverable, and made media-drop fallback explicit without breaking NO_REPLY.
  • What did NOT change (scope boundary): transport-specific reply context handling still stays at the final send edge, and no gateway protocol changes were made.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: WhatsApp outbound media had overlapping behavior owners, and the embedded/final reply path could lose or duplicate assistant-generated media before it reached WhatsApp delivery.
  • Missing detection / guardrail: there was no parity regression suite forcing payload/gateway and auto-reply entrypoints to agree on media classification, caption-first behavior, retry behavior, fallback behavior, and single-send delivery.
  • Contributing context (if known): recent fixes addressed individual path-specific failures, but the multi-owner shape remained and kept allowing outbound-media drift to reappear.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • extensions/whatsapp/src/channel-outbound.test.ts
    • extensions/whatsapp/src/send.test.ts
    • extensions/whatsapp/src/outbound-base.test.ts
    • extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts
    • extensions/whatsapp/src/outbound-payload.contract.test.ts
    • extensions/whatsapp/src/auto-reply/deliver-reply.test.ts
    • src/agents/pi-embedded-runner/run/payloads.test.ts
    • src/auto-reply/reply/agent-runner-payloads.test.ts
    • src/auto-reply/reply/reply-delivery.test.ts
    • src/auto-reply/reply/reply-media-paths.test.ts
  • Scenario the test should lock in: the same outbound media fixture should produce the same classification, primary source selection, caption behavior, retry handling, and single-send delivery across gateway/payload and auto-reply entrypoints.
  • Why this is the smallest reliable guardrail: the regression spans WhatsApp send normalization and final reply payload assembly, so the tests cover both the plugin-local contract and the reply-pipeline points where media could be lost or sent twice.
  • Existing test that already covers this (if any): extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts covers one media reply logging/send path at a higher level.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • WhatsApp outbound media delivery now behaves consistently across gateway/channel sends, payload sends, and auto-reply sends.
  • Assistant auto-replies that emit media directives keep the attachment through final payload delivery.
  • Non-streaming captioned media replies send once with the caption attached.
  • Explicit mediaUrl stays primary when both mediaUrl and mediaUrls are present.
  • The first media item gets the caption and later text remains ordered after media across the shared paths.
  • Wrapped transient WhatsApp send failures retry again instead of failing immediately.
  • Auto-reply fallback remains explicit on media failure and later attachments are still attempted.
  • Channel outbound keeps trimming leading blank lines but preserves intentional leading indentation.

Diagram (if applicable)

Before:
[gateway send] ----> [path A]
[payload send] ----> [path B]
[auto-reply]  ----> [path C]
                     |
                     v
           [different media normalization / caption / retry / fallback]

After:
[gateway send] ----\
[payload send] -----> [canonical outbound media contract] -> [final transport call]
[auto-reply]  ----/             |
                                v
                  [shared media normalization / caption / retry / fallback]

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Linux
  • Runtime/container: Node 22 / pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): WhatsApp plugin
  • Relevant config (redacted): default test config and WhatsApp plugin test fixtures

Steps

  1. Send the same outbound media payload through the gateway/payload path and the auto-reply path.
  2. Include mixed mediaUrl and mediaUrls, a caption/text body, and a wrapped transient send error case.
  3. Exercise assistant-generated MEDIA: replies through the embedded/final reply path.
  4. Verify source selection, caption placement, retry behavior, fallback behavior, and number of send calls.

Expected

  • All WhatsApp outbound entrypoints agree on media presence, primary source, caption-first behavior, retry classification, and fallback behavior.
  • Assistant-generated media replies survive final payload construction and are delivered once.

Actual

  • They now agree through the canonical helper, with regressions added for mixed-field precedence, wrapped retry errors, later-media-after-fallback, channel indentation preservation, final media directive preservation, non-streaming single-send ownership, and media-drop fallback behavior.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • payload/gateway vs auto-reply parity on shared media fixtures
    • mixed mediaUrl + mediaUrls primary-source behavior
    • wrapped transient error retry behavior
    • later media still sends after first-media fallback
    • channel outbound preserves intentional indentation
    • live WhatsApp DM media reply sends one image with one caption
    • live WhatsApp missing-media reply sends one explicit text fallback and no attachment
  • Edge cases checked:
    • blank/whitespace mediaUrls entries
    • OGG Opus normalization
    • text-only short-circuit behavior
    • named/default account-scoped listener resolution through the shared path
  • What you did not verify:
    • every numbered live scenario after the final review-only dedupe adjustment; targeted unit coverage was added for that review case

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk:
    • Canonicalizing behavior or moving non-streaming captioned media to the final path could accidentally change an entrypoint-specific edge case.
  • Mitigation:
    • Added parity regressions across gateway/payload and auto-reply plus focused unit coverage around source selection, retry classification, fallback behavior, channel normalization, final media preservation, and duplicate-send prevention.

@aisle-research-bot

aisle-research-bot Bot commented Apr 21, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Unsanitized filename derived from mediaUrl can enable CRLF/header injection in WhatsApp document upload
2 🟡 Medium Non-idempotent retry of WhatsApp auto-reply sends can cause duplicate messages/media
1. 🟡 Unsanitized filename derived from mediaUrl can enable CRLF/header injection in WhatsApp document upload
Property Value
Severity Medium
CWE CWE-113
Location extensions/whatsapp/src/outbound-media-contract.ts:93-105

Description

deriveWhatsAppDocumentFileName() derives a document fileName from a user-controlled mediaUrl and applies decodeURIComponent without sanitizing control characters or dangerous separators.

  • Input: mediaUrl is taken from outbound payloads/auto-reply media URLs.
  • Transformation: path.posix.basename(parsed.pathname) then decodeURIComponent(fileName).
  • Sink: The resulting fileName is passed into WhatsApp document send payloads (document messages) via Baileys (@​whiskeysockets/baileys) in createWebSendApi, which typically becomes a multipart/form-data filename/metadata field.

A crafted URL such as https://host/%0d%0aX-Injected:1.pdf would produce a filename containing CRLF (\r\n), and %2F can decode to /. If downstream libraries do not strictly validate/escape fileName, this can lead to header/metadata injection or request corruption.

Vulnerable code:

const fileName = path.posix.basename(parsed.pathname);
return fileName ? decodeURIComponent(fileName) : undefined;

Recommendation

Sanitize derived filenames before passing them to messaging libraries.

Suggested approach:

  • Strip CR/LF and other ASCII control chars.
  • Remove path separators (/, \\) after decoding.
  • Enforce a conservative allowlist for characters and a max length.
  • Optionally, fall back to a fixed name (e.g. file) when sanitization changes the value significantly.

Example:

function sanitizeFileName(name: string): string {// remove control chars incl. CR/LF and DEL
  let n = name.replace(/[\u0000-\u001F\u007F]/g, "");// remove path separators
  n = n.replace(/[\\/]/g, "_");// collapse whitespace
  n = n.replace(/\s+/g, " ").trim();// allowlist (tune as needed)
  n = n.replace(/[^A-Za-z0-9._ -]/g, "_");// enforce length
  if (!n) return "file";
  return n.slice(0, 128);
}

const raw = fileName ? decodeURIComponent(fileName) : undefined;
return raw ? sanitizeFileName(raw) : undefined;

Additionally, consider validating sendOptions.fileName in createWebSendApi (defense-in-depth) before using it in Baileys payloads.

2. 🟡 Non-idempotent retry of WhatsApp auto-reply sends can cause duplicate messages/media
Property Value
Severity Medium
CWE CWE-841
Location extensions/whatsapp/src/auto-reply/deliver-reply.ts:72-91

Description

deliverWebReply wraps msg.reply(...) and msg.sendMedia(...) with sendWhatsAppOutboundWithRetry, retrying on a broad transient-error regex (including wrapped errors via formatError). WhatsApp sends are not idempotent at this layer (no idempotency key / no de-dupe on messageId), so a failure after the remote side already accepted the message (e.g., connection reset/closed after send) can result in the same reply being sent multiple times.

Why this is a security/business risk:

  • Spam/billing amplification: an attacker can intentionally induce transient transport failures (or exploit flaky connectivity) while triggering auto-replies, producing duplicate outbound messages/media.
  • Inconsistent behavior: the codebase explicitly asserts elsewhere that outbound sends should not retry “to avoid duplicate sends”, but auto-reply now retries even for wrapped transient errors.

Vulnerable code (auto-reply retries non-idempotent operations):

const sendWithRetry = async (fn: () => Promise<unknown>, label: string, maxAttempts = 3) => {
  return await sendWhatsAppOutboundWithRetry({ send: fn, maxAttempts, ... });
};
...
await sendWithRetry(() => msg.reply(chunk, quote), "text");
...
await sendWithRetry(() => msg.sendMedia({ ... }, quote), "media:..." );

Retry classification is based on error text and can match cases where the send may have succeeded:

return /closed|reset|timed\s*out|disconnect/i.test(formatError(error));

Recommendation

Avoid retrying non-idempotent outbound sends unless you can guarantee idempotency.

Options:

  1. Remove retries for msg.reply/msg.sendMedia in the auto-reply path (match sendMessageWhatsApp behavior).
  2. Implement de-duplication/idempotency:
    • Generate a deterministic idempotency key per auto-reply chunk/media (e.g., hash of replyToId + chunkIndex + payloadHash).
    • Store keys in a short-lived cache and skip re-sends when a key was already successfully sent.
    • Or, if the underlying WhatsApp API/library returns a messageId before errors can occur, persist it and check delivery status before retrying.
  3. Tighten retry conditions to only retry on errors that can be proven to occur before a message is accepted (e.g., local validation/serialization failures), rather than generic network errors.

Example (no retry for sends):

// do not retry WhatsApp sends to avoid duplicates
await msg.reply(chunk, quote);

Example (idempotency cache sketch):

if (await sentCache.has(key)) return;
await msg.reply(chunk, quote);
await sentCache.set(key, true, { ttlMs: 5 * 60_000 });

Analyzed PR: #69813 at commit 5ce22bc

Last updated on: 2026-04-24T03:46:54Z

@openclaw-barnacle openclaw-barnacle Bot added channel: whatsapp-web Channel integration: whatsapp-web size: L maintainer Maintainer-authored PR labels Apr 21, 2026
@greptile-apps

greptile-apps Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces outbound-media-contract.ts as the single source of truth for WhatsApp outbound media normalization (URL deduplication, OGG codec normalization, transient retry), then routes send.ts, outbound-base.ts/sendPayload, and deliver-reply.ts through it. Regression tests cover mixed mediaUrl+mediaUrls precedence, wrapped-error retry classification, post-failure media continuation, and gateway/auto-reply parity.

Confidence Score: 5/5

Safe to merge; one minor defensive-guard suggestion with no impact on normal callers.

All remaining findings are P2 style/robustness suggestions. The canonical contract is correctly wired across all three outbound paths, normalization is idempotent and covered by parity tests, and the wrapped-error retry path is verified by a dedicated test.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/whatsapp/src/outbound-media-contract.ts
Line: 94-118

Comment:
**`throw lastError` can throw `undefined` when `maxAttempts ≤ 0`**

`lastError` is initialized to `undefined` and only assigned inside the `catch` block. If a caller explicitly passes `maxAttempts: 0` (or negative), the loop body never executes and the fallthrough `throw lastError` silently propagates `undefined`, which produces a cryptic "undefined was thrown" at the call site instead of a meaningful error. A guard at the entry or a sentinel initial value would make the failure clearer.

```suggestion
}): Promise<T> {
  const maxAttempts = params.maxAttempts ?? 3;
  if (maxAttempts <= 0) {
    throw new Error(`sendWhatsAppOutboundWithRetry: maxAttempts must be >= 1, got ${maxAttempts}`);
  }
  let lastError: unknown;
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(whatsapp): preserve indented channel..." | Re-trigger Greptile

Comment thread extensions/whatsapp/src/outbound-media-contract.ts
@mcaxtr

mcaxtr commented Apr 21, 2026

Copy link
Copy Markdown
Member Author

Follow-up on bot feedback:

  • Fixed the one concrete regression introduced by this PR in 9480ae0a65 by removing retry-based resend from the gateway/channel outbound path in extensions/whatsapp/src/send.ts and adding a regression test in extensions/whatsapp/src/send.test.ts that locks in single-attempt behavior on transient send failures.
  • The remaining bot items were not addressed in this PR because they fall outside the actual regression scope:
    • unbounded mediaUrls processing and verbose auto-reply retry logging were already present on the auto-reply path before this refactor
    • maxAttempts <= 0 is a defensive hardening edge case with no current non-positive callers
    • filename sanitization and retry-log redaction are plausible hardening follow-ups, but not concrete regressions introduced by this change

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9480ae0a65

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/whatsapp/src/outbound-base.ts
@mcaxtr
mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch 2 times, most recently from c5ed37e to 4188dd5 Compare April 23, 2026 04:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6be4895f4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/whatsapp/src/auto-reply/deliver-reply.ts Outdated
Comment thread extensions/whatsapp/src/outbound-base.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80cd3dbd22

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-media-paths.ts Outdated
@openclaw-barnacle openclaw-barnacle Bot added the agents Agent runtime and tooling label Apr 23, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfc00aec16

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/whatsapp/src/outbound-media-contract.ts Outdated
@mcaxtr
mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch 2 times, most recently from f4b900e to 7506049 Compare April 24, 2026 02:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/openclaw/openclaw/blob/7506049d7af31e0ff1524770c0c66b5616a75469/src/auto-reply/reply/reply-delivery.ts#L147-L150
P1 Badge Deliver media-only block replies when streaming is disabled

When blockStreamingEnabled is false, this handler now never forwards block payloads to onBlockReply, so media-only block replies are dropped. That breaks the lifecycle path where consumePendingToolMediaReply emits attachments at the end of a run: those payloads are not guaranteed to be reconstructible from final assistant text, so they can disappear entirely instead of being sent once. Please restore the media-bearing fallback send path for the non-streaming branch.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-delivery.ts
@mcaxtr
mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from 7506049 to 1e5744a Compare April 24, 2026 03:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e5744a3a6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-media-paths.ts
@mcaxtr
mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from 1e5744a to fd9e485 Compare April 24, 2026 03:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd9e485882

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-delivery.ts
@mcaxtr
mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from fd9e485 to 6f20898 Compare April 24, 2026 03:37
@mcaxtr
mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from 6f20898 to 5ce22bc Compare April 24, 2026 03:40
@mcaxtr
mcaxtr merged commit 18c9831 into main Apr 24, 2026
66 of 69 checks passed
@mcaxtr
mcaxtr deleted the refactor/whatsapp-canonical-outbound-media branch April 24, 2026 04:04
masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request Apr 25, 2026
Fixes openclaw#66053. The WhatsApp adapter previously dropped the audioAsVoice
flag from ChannelOutboundContext, so replies carrying the
[[audio_as_voice]] directive were delivered as document attachments
instead of PTT voice notes.

Layers

- sendMessageWhatsApp: adds audioAsVoice option. When true and the
  loaded media is a verified audio source (isVerifiedAudioSource),
  mediaType is rebuilt from a sanitized MIME with opus codec
  preservation/fallback so the outbound frame reaches WhatsApp as a
  voice note. Non-audio-verified payloads stay on the document path.
- createWhatsAppOutboundBase (outbound-base.ts): forwards audioAsVoice
  through the sendMedia factory option so outbound-adapter.ts no
  longer needs per-adapter wiring (upstream canonicalization landed
  in openclaw#69813).
- deliverWebReply (auto-reply/deliver-reply.ts): routes kind='audio'
  and audioAsVoice-verified media to PTT with sanitized opus
  mimetype; image/video/document branches apply sanitized allowlisted
  mimetypes; document fileName is passed through sanitizeFileName.

Security hardening (applied on top of upstream's
normalizeWhatsAppLoadedMedia helper)

- sanitizeMediaMime rejects control characters (CWE-93) and preserves
  codecs params only in the audio path.
- sanitizeFileName strips ASCII control and bidi/invisible Unicode to
  prevent filename UI spoofing (CWE-451).
- isVerifiedAudioSource gates forceVoiceDelivery so an audioAsVoice
  reply cannot coerce non-audio bytes into a voice-note payload.

Tests (extensions/whatsapp)

- send.test.ts: audioAsVoice=true+audio routing, forceVoiceDelivery
  override guard, mimetype sanitization across kinds, document
  fileName sanitization.
- deliver-reply.test.ts: voice-coercion rejection, control-character
  fallbacks for audio/image/video/document, fileName sanitization,
  audioAsVoice-unset document path.
- outbound-base.test.ts / outbound-adapter.sendpayload.test.ts:
  audioAsVoice propagation through the factory.

Rebased onto upstream/main after openclaw#69813. Audio mimetype canonicalization
("audio/ogg" -> "audio/ogg; codecs=opus") is now owned by
normalizeWhatsAppLoadedMedia; this change layers PTT routing and
security hardening on top.

Closes openclaw#66053
Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: whatsapp-web Channel integration: whatsapp-web maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant