Security
- The group invite-code read, over REST or the MCP
GroupGetInviteCodetool, requires the OPERATOR role; the code is a transferable join capability, so a VIEWER key can no longer extract it (GHSA-45fh-xj7x-vj2x). Thanks Matija Petronijević for the report. - The amd64 image ships Chrome for Testing 153.0.8010.36 instead of 146.0.7680.31, picking up the browser security fixes released since (the arm64 image uses the Debian chromium package).
- The
session.qrWebSocket event reaches only OPERATOR and ADMIN keys, matchingGET /api/sessions/{sessionId}/qr; a VIEWER key subscribed by name or through a wildcard no longer receives the pairing QR (GHSA-m427-j4h4-9qwj). - An integration ingress route verified with
shared-secretno longer stores the instance secret from its declared header; the value is redacted in the persisted event, the queued job, the dead-letter row and theingress:errorhook payload. - Baileys sessions with an HTTP, HTTPS or SOCKS5 proxy fetch through the proxy instead of connecting direct: inbound media, the WhatsApp Web version lookup, the history-sync and app-state payloads of the initial sync, and a product card's image URL. With a SOCKS4 proxy, which the HTTP client cannot use, inbound media is skipped and arrives as the omitted marker, the version lookup falls back to the bundled version, and the initial-sync payloads and product image are still fetched directly.
Added
- Inbound commerce messages arrive typed
orderandproductinstead of a bodylessunknown, on both engines, and are accepted by webhook and automation-rule message-type filters (#1547). Thanks @m7fz7. - The JavaScript, Python, Go and Java SDKs type the
orderandproductmessage types, and the PythonChatHistoryMessagecarries their blocks with required fields and enums matching the contract. GET /api/sessionsaccepts anamequery parameter that returns only the session with that exact name, also on the MCPSessionFindAlltool and the JavaScript, Python, Go, Java and PHP SDKs (#1594). Thanks @rivenash for the request.
Changed
- The Italian (
it) dashboard translates the session proxy Save button, the webhook chat-kind filter label and the warning shown when a backup export leaves out media (#1583). Thanks @albanobattistella. webhooks.deliveryFailuresreturns a typedWebhookDeliveryFailurelist in the JavaScript, Python, Go and Java SDKs; Go and Java callers that handled the old untyped value must update.- From-source minimum Node.js rises from 22.13 to 22.19, the floor of the bundled
undici.
Fixed
- Engine auth directories are named after the session id instead of the session name, so two sessions whose names differ only in letter case no longer share one WhatsApp login, or wipe each other's, on a case-insensitive filesystem such as macOS APFS, Windows, or a Docker Desktop bind mount of either (#1597). Existing directories are renamed at the first boot after the upgrade.
- The Baileys live path drops a message made only of sender-key distributions or message-history notices instead of delivering it as a bodyless
unknownmessage.received; other messages it cannot type still arrive asunknown(#1568). Thanks @berodcdev for the report. - A Baileys reconnect loop is observable through
lastErroron the session, asession.reconnect_loopwebhook every fifth attempt and reconnect metrics; a QR left unscanned is not reported as one (#1546). Thanks @OdaiAhmed99 for the report. - A Baileys connection attempt refused at the WebSocket upgrade is closed and retried instead of leaving the session at
initializing(#1546). Thanks @OdaiAhmed99 for the report. - The dashboard session card keeps the phone number, session id and last-active time while a linked session reconnects, instead of the pairing placeholder (#1546). Thanks @OdaiAhmed99 for the report.
- The Sessions page reports a dead live-event feed. When the feed recovers from a gap, the Sessions page re-reads its list and the Chats page refetches the open thread and contact statuses; a feed that never connected counts as a gap only once it has failed and shown the reconnect banner.
- A Baileys media download aborted at
MEDIA_DOWNLOAD_MAX_BYTESreports the bytes received assizeBytes, and a timed-out one its declared size, instead of the cap. - The webhook docs state that at the default limits media above about 768 KiB reaches webhooks as the omitted marker, and how to raise both limits (#1569). Thanks @Magnarks for the report.
- The takeover sweep marks as disconnected any session left
ready,initializing,authenticatingoraction_requiredby a node that never returned, regardless ofAUTO_START_SESSIONS. - The takeover sweep also marks a lapsed
qr_readysession with no phone as disconnected; one with a phone keeps its status. - Reconnect on a dashboard session card that reads
initializingorqr_readywith no engine loaded starts the session, instead of opening a QR modal that never receives a code. - Branch Docker images (
:main, sha tags) rebuild the production stage without the build cache, so they cannot serve stale OS packages. - The Docker image upgrades the Debian packages inherited from the digest-pinned
node:22-slimbase at build time, so security fixes published after the base snapshot reach them; this clears CVE-2026-86145 and CVE-2026-89161 inlibpcre2-8-0. - The Message Tester's bulk-recipients file picker refuses files over 2 MB before reading them.
restore.shrefuses to overwrite a live database without--forceeven when the operator's sqlite3 rc file changes its output format.- A misspelled
LOG_LEVELfails the boot naming the accepted values, instead of silently logging at info. - Dependabot can open better-sqlite3 13.x patch and minor updates again; the freeze now starts at v14.
- A request forwarded to the node that owns its session answers
504or502, not503, when the forward times out or breaks after the request was sent, so a client retrying on503no longer repeats a send the owner may have carried out;503remains for an owner that could not be reached at all. - The dashboard Logs page and its sidebar entry are shown to admin keys only, matching the ADMIN-only
GET /api/auditit reads. - The dashboard Sessions page hides Show QR for viewer keys, since the QR is operator-only.
- The dashboard Sessions page re-reads the session list once after a failed read, as soon as live updates are connected, and regains that retry after a successful read, instead of keeping the error until a reload.
- The dashboard Templates page shows a load or permission error when the template list cannot be read, instead of "No templates saved".
- The dashboard Webhooks Configured card shows a placeholder instead of 0 when the webhook list cannot be read.
- Dashboard message search ignores a response that arrives after a newer query, so stale results no longer replace the current ones.
- A session whose automatic reconnect fails to relaunch the engine (a network, DNS or browser launch error) keeps retrying with backoff instead of stopping in
faileduntil restarted by hand; an authentication failure or a stale browser profile still ends infailed(#1580). - The
504a start returns when the engine does not finish initializing names every possible cause, an unreachable WhatsApp Web, network or session proxy and a browser stalled during startup, instead of ruling the network out (#1601). - A session that runs out of reconnect attempts fires the
session:errorplugin hook when it lands infailed. - A session that runs out of reconnect attempts keeps the last attempt's failure reason in
lastErrorand in thesession:errorhook, after the attempts message. - A stop during the retry delay after a transient start failure is no longer undone by the retry; a stop or delete the ownership fence refused leaves the retry alone.
- In a multi-node deployment, a start cut short by a concurrent stop releases its claim, so a peer no longer adopts and restarts the stopped session.
- A stop or delete that fails on a database error no longer blocks the session's next automatic reconnect or makes a later start answer "already started".
- A logout or force-kill refused as "not started" leaves the session's claim untouched, so a crashed node's session stays visible to the takeover sweep.
GET /api/sessions/:sessionId/presence/:chatIdreturnsnullonce the session has no running engine, instead of the last presence reported before a stop, logout, force-kill or failure.- An explicit
maxReconnectAttemptsis honoured and the reconnect delay is capped at 5 minutes; the budget used to restart once the backoff passed 5 minutes, so a limit above about 6 attempts was never reached and the documented 1-hour cap never applied. - The webhook delivery reconciler no longer replays a delivery that is still waiting for a dispatch slot or retrying on the node that dispatched it, which sent a duplicate outside
WEBHOOK_DISPATCH_CONCURRENCYand could close a slow delivery as failed while it was still running. session.reconnect_loopwebhooks carry an idempotency key salted per occurrence, so an alert from a later outage that reaches the same attempt count is no longer deduplicated onto the earlier one or left without a delivery record.- A bulk batch sends each message through the session's current engine, so a reconnect or restart mid-batch no longer fails every remaining message.
POST /messages/send-bulkanswers 400 for an item with an emptychatId, a text item without text, or a media item without aurlorbase64under its type; such items used to be accepted with 202 and fail later.POST /messages/send-bulkanswers 400, not 500, when a concurrent request already created the samebatchId.PUT /templates/:idanswers 400, not 500, for anullnameorbody.GET /api/searchanswers 400, not 500, for a fractionallimitoroffset.POST /messages/send-templatewithouttemplateIdortemplateNameanswers 400 instead of 404.- The label upsert documentation no longer says omitted fields are kept: the write replaces the whole label, so an omitted name or colour is not preserved.
- Restoring a PostgreSQL backup into SQLite stores creation and update timestamps in SQLite's own format, so restored pending webhook deliveries and ingress events are replayed on the day they were created instead of from the next UTC day.
- The
POST /api/infra/import-dataschema and the API docs state 16 migration tables, including thechatStatesandwebhookOutboxEventskeys the restore already clears. GET /api/infra/storage/exportstreams files into the archive one at a time instead of loading the whole media store into memory first, so exporting a large local or S3 store no longer exhausts memory.- A Baileys session behind an HTTP(S) proxy that never answers CONNECT no longer leaves an open connection to the proxy on every reconnect attempt.
- A Baileys inbound media download that passes
MEDIA_DOWNLOAD_TIMEOUT_MSbefore its stream opens stops instead of buffering in the background outsideINBOUND_MEDIA_CONCURRENCY. BAILEYS_CHAT_STATE_CACHE_MAXis listed in.env.exampleand forwarded by both bundled Compose files, so the Baileys chat-state cache cap can be raised from.env.
Dependencies
multer2.2.0 to 2.3.0 via an override, closing three high-severity multipart denial-of-service advisories. It ships in the runtime tree.
Upgrade notes (behavior changes)
- whatsapp-web.js: back up
sessions/before upgrading. A rollback to an image with an older browser major deletes the stored WhatsApp logins unlesssessions/is restored from that backup, and a session first paired after the upgrade must be paired again (seedocs/11-operational-runbooks.md). Baileys sessions are unaffected. - Back up
sessions/andbaileys/before upgrading. The first boot renames each session's auth directory from the session name to its id; a rollback to 0.23.4 or earlier looks for the name-keyed directory, finds nothing, and starts every session on both engines at a QR code unless both directories are restored from that backup (seedocs/11-operational-runbooks.md). - whatsapp-web.js on a non-container install: confirm no Chromium survived the stop before starting 0.23.5. The orphan sweep matches the session id from this release on, so a browser orphaned by a hard kill of the older version is no longer recognised and would hold the same profile as the one launched next to it.
- The amd64 image moves from Chrome for Testing 146 to 153, so every amd64 rollback to 0.23.4 or earlier crosses a browser major; the arm64 image runs the chromium Debian ships at build time, whose major can differ between releases.
- A
LOG_LEVELother thanerror,warn,info,debugorverbosenow stops the boot instead of logging at info. - Multi-node deployments run the lapsed-status correction even with
AUTO_START_SESSIONSoff, so every node needs a synced clock and, on PostgreSQL, one time zone without daylight saving (TZ=UTCrecommended); otherwise live sessions can be marked disconnected (seedocs/13-horizontal-scaling.md). - A session with an explicit
maxReconnectAttemptsnow stops infailedonce those attempts run out during an outage; before, a limit above about 6 at the default base delay was never reached and the session retried indefinitely. - A dashboard or client signed in with a
VIEWERkey no longer receives the pairing QR over the/eventsWebSocket, matching the OPERATOR roleGET /api/sessions/{sessionId}/qralready required. - Installing from source now needs Node.js 22.19 or newer; the published Docker image is unaffected.
- Python SDK:
ChatHistoryMessagemarks the keys the contract always sends as required and narrowstypeandkindto literals, so a hand-built partial dict or a plainstrassigned to either no longer type-checks.