v0.5.0
A security & reliability hardening release. One behavior change (the reason for the minor bump):
the contact / group / chat list endpoints now paginate with a default cap of 1000 items — opt into
limit/offset; accounts with fewer than 1000 items are unaffected. Everything else is hardening and
correctness: time-bounded SSRF DNS resolution, validated webhook custom headers (blocks CR/LF
injection), Swagger off by default in production, boot-time validation of numeric env vars and of a
SQLite data/main path collision, plugin reads gated to ADMIN, a session-scoped key no longer denied on
non-session routes, no resurrection of a session stopped mid-startup, a hardened dashboard config-save
path (browser-flag parsing + 0600 secret file), and cleaner fresh-install schema.
Changed
- The contact, group, and chat list endpoints are now paginated (default cap 1000).
⚠️ Behavior
change.GET /sessions/:id/contacts,/groups, and/chatspreviously serialized the operator's
entire address book / group / chat set into one response — a heap/GC hazard for very large
accounts. They now accept optionallimit(clamped[1, 1000]) andoffsetquery params, and
default to returning at most 1000 items when nolimitis given. Accounts under 1000 items are
unaffected; larger accounts page withoffset. Chats are returned most-recent first, so a
capped response is the newest chats rather than an arbitrary slice. In-process callers (plugins
using the engine directly) still receive the full set. (#401) - Fresh databases no longer create the unused
api_keys/audit_logstables on the data
connection. Those auth/audit tables belong solely to the separate "main" SQLite connection, but
the data-connection baseline migration also created them (with a stalekeyPrefixwidth), leaving
dead, unused tables on the data database. New installs are now clean. Existing installs are
unaffected — an already-applied migration is never re-run, so their harmless leftover tables remain
and no destructive drop is performed. (#400)
Fixed
- Browser launch flags saved from the dashboard are now applied correctly. The Infrastructure
form persists the Puppeteer/Chromium arguments space-separated, but the engine config parser only
split on commas — collapsing every flag into a single malformed argv token, so--no-sandbox(and
any other flag) was silently never applied. In a hardened/containerized environment that can wedge
session startup. The parser now accepts either delimiter, and an already-saved space-separated value
is repaired on the next boot. (#397) - A session-restricted API key is no longer wrongly denied on non-session routes. The guard
derived the session for a key'sallowedSessionsscope from the:idroute param, but:idis
also the resource id on unrelated routes (e.g.auth/api-keys/:id,plugins/:id) — so a
session-scoped key got a spurious401there. Session scoping is now applied only where:id
actually denotes a session; enforcement on the realsessions/:id/...routes is unchanged. (#398) - Boot is now rejected when the SQLite
DATABASE_NAMEcollides with the internal main database
file. The auth/audit ("main") and application ("data") connections must be separate SQLite files;
pointingDATABASE_NAMEat./data/main.sqliteran two connections — each with its own migration
ledger and synchronize policy — against one file, risking schema divergence and lock contention.
Startup validation now fails fast with a clear message (paths are normalized, so relative spellings
of the same file are caught). Postgres is unaffected (itsDATABASE_NAMEis a bare db name). (#399) - Numeric environment variables are validated at boot. The rate-limit windows/limits, webhook
timeout/retry settings, and the database pool size were parsed with an unboundedparseInt; a
non-integer value (e.g.RATE_LIMIT_SHORT_LIMIT=abc) becameNaNand silently disabled the
corresponding limit. Startup now rejects a non-negative-integer violation with a clear message,
consistent with the existing port validation. (#402) - The whatsapp-web.js engine now detects remote media URLs case-insensitively. A media
data
string was treated as a URL only with a lowercasehttp:///https://prefix, so a mixed-case
scheme (e.g.HTTPS://…) was mistaken for base64 instead of being fetched through the SSRF-guarded
path — diverging from the Baileys engine. Both engines now use the same case-insensitive check. (#404) - A session stopped or deleted mid-startup is no longer resurrected to
READY. Ifstop/delete
landed whilestart()was awaiting the engine'sinitialize(), the freshly-created engine was left
registered and running.start()now re-checks the stopping flag after initialization and tears the
engine down (mirroring the existing reconnect guard), so a concurrent stop/delete wins. (#405)
Security
- DNS resolution in the SSRF guard is now bounded by a deadline. The guard resolved a hostname
with an unbounded lookup, so a hanging or very slow resolver could pin a worker indefinitely. The
lookup now races a deadline (default 10s, overridable viaSSRF_DNS_TIMEOUT_MS) and fails closed
with a clear error on expiry. Healthy resolvers are unaffected. (#404) - Custom webhook headers are now validated as a flat, control-character-free string map. The
headersfield accepted any object shape with no per-value checks, so a value containingCR/LF
could attempt header injection into the outbound webhook request, and non-string values silently
broke delivery. Creation/update now reject invalid header names, non-string or control-character
values, and over-large maps (max 50 entries, value max 1024 chars). The delivery-time reserved-name
filter is unchanged. (#403) - Swagger UI (
/api/docs) now defaults OFF in production. The interactive API schema was served
unauthenticated by default everywhere; it is reconnaissance surface. It remains on outside
production and can be re-enabled in production withENABLE_SWAGGER=true(and is still disabled
anywhere withENABLE_SWAGGER=false). The startup banner only advertises the docs URL when it is
actually served. (#402) - Plugin inventory, detail, and health reads now require the ADMIN role.
GET /plugins,
GET /plugins/:id, andGET /plugins/:id/healthwere readable by any authenticated key (including
the read-only VIEWER role), exposing installed plugin versions, non-secret configuration, and
health/error text. They now require ADMIN, matching the plugin write routes and the infrastructure
endpoints. (Secret config values were — and remain — redacted regardless.) (#398) - The dashboard-generated env file is now written owner-only (
0600). Saving Infrastructure
configuration wrotedata/.env.generated— which can hold the database, S3, and Redis credentials —
with default permissions (world-readable0644) until the next restart re-tightened it. It is now
written0600at save time through the same owner-only helper used for the generated env at first
boot, closing the exposure window on shared or bind-mounted hosts. (#397)