You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Problem: createWebSendApi document branch applied only .trim() to sendOptions?.fileName before passing it to Baileys; a caller supplying a fileName containing CR, LF, or other C0/DEL control characters could embed those bytes in the outbound document message metadata, enabling CWE-93-style header injection via Baileys' HTTP-like send path.
Why it matters: The fileName value originates from documentMessage.fileName on the inbound WhatsApp wire (forwarded through monitor.ts), so a message sender can supply arbitrary bytes. The old code did not sanitize this surface.
What changed: resolveWhatsAppDocumentFileName() in extensions/whatsapp/src/document-filename.ts now strips \x00-\x1f and \x7f (all C0 control characters + DEL) from fileName before .trim(), and falls back to a MIME-derived filename (e.g., "file.pdf" for application/pdf) when the stripped result is empty. Three tests in extensions/whatsapp/src/document-filename.test.ts cover CRLF injection, control-character strip, and the all-control fallback. The lint-suppression allowlist in test/scripts/lint-suppressions.test.ts records the intentional no-control-regex suppress comment.
Root cause: The document-send branch only called .trim(), which removes leading/trailing whitespace but leaves CR, LF, and other control characters intact anywhere in the string.
Missing detection / guardrail: No test for injection payloads in fileName; existing tests passed only clean filenames.
Contributing context (if known): fileName propagates verbatim from the inbound WhatsApp documentMessage.fileName field through monitor.ts → sendOptions.fileName, so any peer can supply arbitrary bytes.
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/document-filename.test.ts
Scenario the test should lock in: (1) CRLF-containing fileName reaches resolveWhatsAppDocumentFileName with CR/LF stripped; (2) C0/DEL control characters stripped, valid chars preserved; (3) all-control filename falls back to MIME-derived name (e.g., "file.pdf" for application/pdf).
Why this is the smallest reliable guardrail: The sanitizer is a pure function; injection can be asserted by calling it directly with attacker-controlled inputs and verifying the return value.
Existing test that already covers this (if any): None — no prior test exercised control-character inputs.
If no new test is added, why not: N/A — 3 tests added.
User-visible / Behavior Changes
Documents sent with filenames containing control characters will have those characters stripped before delivery. Filenames consisting entirely of control characters will fall back to a MIME-derived filename (e.g., "file.pdf" for application/pdf). Clean filenames are unaffected.
What you did not verify: live Baileys wire behavior (requires a running WhatsApp session); no E2E or live test run.
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: A fileName containing valid text that begins/ends with whitespace (but no control chars) is already trimmed by the old code. The new code strips control chars first, then trims — same trim behavior, narrower bypass window.
Mitigation: The regression test "returns plain filename unchanged when no control characters present" passes a clean "document.pdf" and verifies it reaches the sanitizer unchanged.
Real behavior proof
Behavior addressed: WhatsApp outbound document fileName values containing C0 control characters and DEL — including CR, LF, NUL, US, and DEL — are stripped before reaching the Baileys document payload. If the stripped filename becomes empty, the existing MIME-aware fallback is used. Clean filenames are preserved.
Real environment tested: Linux/Ubuntu, Node v22.22.2, pnpm 11.1.0, PR HEAD 1f377fe0ed. PR source modules loaded directly from this branch via tsx — extensions/whatsapp/src/inbound/send-api.ts (createWebSendApi) and extensions/whatsapp/src/document-filename.ts (resolveWhatsAppDocumentFileName). The socket is a real Baileys 7.0.0-rc11 WhatsApp Web client (makeWASocket + useMultiFileAuthState), paired via requestPairingCode to a dedicated proof account that owns the recipient number, and the recipient is the same account's own self chat. No mock sock, no fake recorder: a 1-deep sendMessage spy wrapper captures payload.fileName / payload.mimetype and then unconditionally awaits the original Baileys sock.sendMessage bound to the same socket instance, so every captured send is also a real send.
Exact steps or command run after this patch:
From a clean checkout of this branch on a host with pnpm install completed, and with an own WhatsApp account number available for proof-only pairing.
First, create a proof-only auth store outside the repo, mode 0700:
Then run the proof script interactively. The script connects a real Baileys 7.0.0-rc11 WhatsApp Web socket, requests a pairing code (it handles the restartRequired (515) re-connect after the phone accepts the code), then routes ONE document send through createWebSendApi (the PR-modified boundary). sock.sendMessage is wrapped as a spy that LOGS payload.fileName and ALWAYS delegates to the real Baileys sock.sendMessage. The phone number is read from env, never echoed; the recipient JID and WA message id are not printed and are redacted in the public proof. Enter the printed pairing code on the primary phone (WhatsApp → Linked Devices → Link with phone number instead).
Output captured on PR HEAD 1f377fe0ed, Node v22.22.2, baileys 7.0.0-rc11, one live Baileys session paired against the proof account. The phone number is never echoed; the recipient JID and WA message id are fully redacted.
The five control bytes present in the input filename hex — \x0d (CR), \x0a (LF), \x00 (NUL), \x1f (US), \x7f (DEL) — are all absent from the payload hex handed to real Baileys sock.sendMessage. resolveWhatsAppDocumentFileName collapsed the dirty filename to "oc-proof-injected.txt" before createWebSendApi constructed the document payload, and the same value reached the real WhatsApp Web socket (the spy wrapper delegated, the socket returned a real message id). For an all-control input, the existing MIME-aware fallback path is exercised by the unit tests in extensions/whatsapp/src/document-filename.test.ts (case "3. all-control → MIME fallback"); the live boundary capture above covers the non-empty injection case, which is the realistic CWE-93 vector.
Phone-side verification also confirmed the delivered WhatsApp document displayed as oc-proof-injected.txt with caption OpenClaw #77114 proof; no phone number, JID, contact list, or message id is included in the public proof.
What was not tested:
The other WhatsApp content paths (image / audio / video / mentions / quote / caption) are not exercised by this proof. Caption / mentions sanitization is tracked separately (PR-CANDIDATES C-4).
Inbound documentMessage.fileName propagation through media.ts / monitor.ts is observed only indirectly: the inbound surface is what supplies attacker-controlled bytes, and this PR's sanitizer sits at the outbound boundary.
The proof socket is short-lived (paired, one send, closed). Long-running session behavior is not exercised.
No phone number, full recipient JID, full WA message id, credential, token, or chat-list metadata was logged or captured. The proof-only Baileys auth state lives in a directory outside the repo (mode 0700) and is never committed.
Summary
createWebSendApidocument branch applied only.trim()tosendOptions?.fileNamebefore passing it to Baileys; a caller supplying a fileName containing CR, LF, or other C0/DEL control characters could embed those bytes in the outbound document message metadata, enabling CWE-93-style header injection via Baileys' HTTP-like send path.documentMessage.fileNameon the inbound WhatsApp wire (forwarded throughmonitor.ts), so a message sender can supply arbitrary bytes. The old code did not sanitize this surface.resolveWhatsAppDocumentFileName()inextensions/whatsapp/src/document-filename.tsnow strips\x00-\x1fand\x7f(all C0 control characters + DEL) fromfileNamebefore.trim(), and falls back to a MIME-derived filename (e.g.,"file.pdf"forapplication/pdf) when the stripped result is empty. Three tests inextensions/whatsapp/src/document-filename.test.tscover CRLF injection, control-character strip, and the all-control fallback. The lint-suppression allowlist intest/scripts/lint-suppressions.test.tsrecords the intentionalno-control-regexsuppress comment.B-4), caption Bidi/control strip (C-4), inboundmedia.ts/monitor.tschain,src/media/sanitize-filename.ts(B-1, blocked on PR fix(media): strip control and bidi characters from outbound filenames #72973), CHANGELOG.md, docs.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
.trim(), which removes leading/trailing whitespace but leaves CR, LF, and other control characters intact anywhere in the string.fileName; existing tests passed only clean filenames.fileNamepropagates verbatim from the inbound WhatsAppdocumentMessage.fileNamefield throughmonitor.ts→sendOptions.fileName, so any peer can supply arbitrary bytes.Regression Test Plan (if applicable)
extensions/whatsapp/src/document-filename.test.tsresolveWhatsAppDocumentFileNamewith CR/LF stripped; (2) C0/DEL control characters stripped, valid chars preserved; (3) all-control filename falls back to MIME-derived name (e.g.,"file.pdf"forapplication/pdf).User-visible / Behavior Changes
Documents sent with filenames containing control characters will have those characters stripped before delivery. Filenames consisting entirely of control characters will fall back to a MIME-derived filename (e.g.,
"file.pdf"forapplication/pdf). Clean filenames are unaffected.Diagram (if applicable)
Security Impact (required)
Yes, explain risk + mitigation: N/ARepro + Verification
Environment
Steps
sendMessage(jid, "doc", buffer, "application/pdf", { fileName: "evil.pdf\r\nX-Injected: bad" }).sendMessagemock receivesfileName: "evil.pdf\r\nX-Injected: bad"(injection present).fileName: "evil.pdfX-Injected: bad"(CR/LF stripped).Expected
fileNamepassed to Baileys contains no C0 control characters or DEL.Actual (before fix)
CR and LF bytes survived
.trim()in the middle of the string.Evidence
Human Verification (required)
\r\n\x00\x1fall-control →"file.pdf"(MIME fallback for application/pdf);"invoice.pdf"unchanged;"a\x00b\x1fc\x7f.pdf"→"abc.pdf".Review Conversations
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
Risks and Mitigations
"returns plain filename unchanged when no control characters present"passes a clean"document.pdf"and verifies it reaches the sanitizer unchanged.Real behavior proof
Behavior addressed: WhatsApp outbound document
fileNamevalues containing C0 control characters and DEL — including CR, LF, NUL, US, and DEL — are stripped before reaching the Baileys document payload. If the stripped filename becomes empty, the existing MIME-aware fallback is used. Clean filenames are preserved.Real environment tested: Linux/Ubuntu, Node v22.22.2, pnpm 11.1.0, PR HEAD
1f377fe0ed. PR source modules loaded directly from this branch viatsx—extensions/whatsapp/src/inbound/send-api.ts(createWebSendApi) andextensions/whatsapp/src/document-filename.ts(resolveWhatsAppDocumentFileName). The socket is a real Baileys 7.0.0-rc11 WhatsApp Web client (makeWASocket+useMultiFileAuthState), paired viarequestPairingCodeto a dedicated proof account that owns the recipient number, and the recipient is the same account's own self chat. No mock sock, no fake recorder: a 1-deepsendMessagespy wrapper capturespayload.fileName/payload.mimetypeand then unconditionallyawaits the original Baileyssock.sendMessagebound to the same socket instance, so every captured send is also a real send.Exact steps or command run after this patch:
From a clean checkout of this branch on a host with
pnpm installcompleted, and with an own WhatsApp account number available for proof-only pairing.First, create a proof-only auth store outside the repo, mode 0700:
Then run the proof script interactively. The script connects a real Baileys 7.0.0-rc11 WhatsApp Web socket, requests a pairing code (it handles the
restartRequired (515)re-connect after the phone accepts the code), then routes ONE document send throughcreateWebSendApi(the PR-modified boundary).sock.sendMessageis wrapped as a spy that LOGSpayload.fileNameand ALWAYS delegates to the real Baileyssock.sendMessage. The phone number is read from env, never echoed; the recipient JID and WA message id are not printed and are redacted in the public proof. Enter the printed pairing code on the primary phone (WhatsApp → Linked Devices → Link with phone number instead).Evidence after fix:
Output captured on PR HEAD
1f377fe0ed, Node v22.22.2, baileys 7.0.0-rc11, one live Baileys session paired against the proof account. The phone number is never echoed; the recipient JID and WA message id are fully redacted.Observed result after fix:
The five control bytes present in the input filename hex —
\x0d(CR),\x0a(LF),\x00(NUL),\x1f(US),\x7f(DEL) — are all absent from the payload hex handed to real Baileyssock.sendMessage.resolveWhatsAppDocumentFileNamecollapsed the dirty filename to"oc-proof-injected.txt"beforecreateWebSendApiconstructed the document payload, and the same value reached the real WhatsApp Web socket (the spy wrapper delegated, the socket returned a real message id). For an all-control input, the existing MIME-aware fallback path is exercised by the unit tests inextensions/whatsapp/src/document-filename.test.ts(case "3. all-control → MIME fallback"); the live boundary capture above covers the non-empty injection case, which is the realistic CWE-93 vector.Phone-side verification also confirmed the delivered WhatsApp document displayed as
oc-proof-injected.txtwith captionOpenClaw #77114 proof; no phone number, JID, contact list, or message id is included in the public proof.What was not tested:
C-4).documentMessage.fileNamepropagation throughmedia.ts/monitor.tsis observed only indirectly: the inbound surface is what supplies attacker-controlled bytes, and this PR's sanitizer sits at the outbound boundary.0700) and is never committed.