Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,21 @@ WEBHOOK_SSRF_PROTECT=true
# Server-side media size/time limits:
# MEDIA_DOWNLOAD_MAX_BYTES=52428800 # cap remote-URL sends AND inbound media (default 50 MiB; oversized inbound media is dropped, message kept)
# MEDIA_DOWNLOAD_TIMEOUT_MS=30000 # abort a slow media download (default 30s)
# Storage import/export limits (ADMIN /infra/storage/* endpoints):
# STORAGE_IMPORT_MAX_BYTES=209715200 # per-entry cap for a tar.gz import; aborts on overflow (default 200 MiB)
# STORAGE_IMPORT_MAX_ENTRIES=100000 # max entries in an import archive; aborts beyond this (default 100000)
# STORAGE_EXPORT_TTL_MS=3600000 # auto-delete an export archive after this long (default 1h)

# =============================================================================
# RATE LIMITING
# RATE LIMITING (all TTLs are in MILLISECONDS)
# =============================================================================
RATE_LIMIT_TTL=60 # Time window in seconds
RATE_LIMIT_MAX=100 # Max requests per window
# The "medium" tier is the one enforced on the API; short/long are optional extra tiers.
RATE_LIMIT_MEDIUM_TTL=60000 # window in ms (default 60000 = 60s)
RATE_LIMIT_MEDIUM_LIMIT=100 # max requests per window
# RATE_LIMIT_SHORT_TTL=1000 # 1s burst window (default)
# RATE_LIMIT_SHORT_LIMIT=10
# RATE_LIMIT_LONG_TTL=3600000 # 1h window (default)
# RATE_LIMIT_LONG_LIMIT=1000

# =============================================================================
# PLUGINS
Expand Down
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.3] - 2026-06-19

A security-hardening and reliability release: outbound-request and storage hardening, plugin/message persistence
fixes, delivery-status and concurrency correctness, and lifecycle robustness — including a **force-kill recovery
for stuck sessions** and its dashboard button. **No breaking changes** for a correctly-configured deployment; the
only behavior change to note is that a misconfigured `ENGINE_TYPE`/`STORAGE_TYPE` now fails fast at boot instead
of silently falling back to the default.

### Added

- **Force-kill a stuck session.** `POST /sessions/:id/force-kill` (OPERATOR) recovers a session whose engine is
wedged and won't respond to a normal stop/delete: the whatsapp-web.js engine **SIGKILLs its own Chromium
process directly** (never a process-wide kill that could take down other sessions), then best-effort tears the
client down; the Baileys engine ends its socket. The teardown is time-bounded and isolated, and the session is
left `DISCONNECTED` and restartable. (#352)
- **Dashboard "Kill Stuck" button.** Session cards in a `failed` state get a Kill Stuck action that confirms,
then calls the force-kill endpoint above. (#351)

### Security

- **Outbound webhook and media fetches are pinned to the SSRF-validated IP.** The host check and the actual
connection previously resolved DNS independently, leaving a DNS-rebinding window; the connection now reuses
the exact vetted address (preserving the hostname for TLS SNI/`Host`, with A-record failover) across webhook
delivery (direct/queued/test) and server-side media downloads. (#338)
- **IPv6 SSRF blocklist closes embedded-IPv4 gaps** (6to4 `2002::/16`, NAT64 `64:ff9b::/96`, IPv4-compatible
`::/96`); the LibreTranslate plugin client is SSRF-guarded; per-session `proxyUrl` is validated as an
`http(s)`/`socks4`/`socks5` URL. (#344)
- **Secret/auth hardening.** Generated secret files (`data/.env.generated`, `data/.api-key`) are written `0600`;
an opt-in `API_KEY_PEPPER` hashes API keys with HMAC-SHA256; `allowedIps` entries are validated as IPv4/CIDR;
the queue dashboard (Bull Board) auth uses the same trusted-proxy IP model as the API; the production
secret-guard inspects the canonical S3 variables. (#345)
- **Storage import/key hardening.** A `tar.gz` import is bounded against decompression bombs (per-entry byte cap
+ entry-count cap); storage-key containment is enforced at the backend-agnostic boundary so the S3 path
inherits it; a plugin's `ctx.storage` is sandbox-contained against `..` traversal. (#346)

### Fixed

- **Webhook subscriptions for session lifecycle events now deliver.** `session.status`, `session.qr`,
`session.authenticated`, `session.disconnected` were accepted on subscribe but never dispatched; they now fire
from the engine lifecycle (the n8n docs are corrected to the real event names). (#335)
- **Plugin enable/disable and configuration now persist** across restarts (they previously updated only
in-memory state while the API reported success). Plugins are not auto-enabled on boot for safety; their saved
configuration is preserved. (#339)
- **Bulk-sent messages are recorded, their errors no longer leak internal addresses, and a running batch can be
cancelled across instances.** (#340)
- **Forwarded messages on the whatsapp-web.js engine report a real WhatsApp message id**, so their delivery
status advances (the synthetic `fwd_<id>` could never match an ack). (#341)
- **A late delivery/read receipt is no longer lost** (the ack retries once when it arrives before the send's id
is committed); **concurrent reactions no longer overwrite each other** (serialized per message); a plugin hook
that reports an error no longer has its partial output applied; a failed ack write is logged with context. (#348)
- **Storage export no longer accumulates copies on the data volume** — it writes under `data/exports/` with a
TTL sweep and an async read (instead of a synchronous read that blocked the event loop). (#346)
- **`WEBHOOK_TIMEOUT` is honored on the queued and test delivery paths** (not just the deprecated direct one);
graceful shutdown is bounded (a half-open Redis socket can't block `app.close()`); unsupported status/catalog
operations return a consistent `501`; a misconfigured `ENGINE_TYPE`/`STORAGE_TYPE` fails fast at boot. (#350)

### Changed

- **The `/api/metrics` scrape is memoized for a few seconds** so back-to-back scrapes don't each run a full
session scan plus aggregates; removed a dead branch in the WebSocket connect handler. (#350)

### Documentation

- Added a **phone-number pairing** example. (#343)
- Documented the webhook `idempotencyKey`/`deliveryId` fields (body + `X-OpenWA-*` headers) and the dedup rule;
corrected the `.env.example` rate-limit variable names (`RATE_LIMIT_MEDIUM_TTL`/`_LIMIT`, in milliseconds). (#350)

## [0.4.2] - 2026-06-19

Bug-fix and hardening release: access-control tightening, session-lifecycle resilience, data-migration
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<p align="center">
<a href="https://github.com/rmyndharis/OpenWA/actions/workflows/ci.yml"><img src="https://github.com/rmyndharis/OpenWA/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"/></a>
<img src="https://img.shields.io/badge/version-0.4.0-blue.svg" alt="Version"/>
<img src="https://img.shields.io/badge/version-0.4.3-blue.svg" alt="Version"/>
<img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"/>
<img src="https://img.shields.io/badge/node-22_LTS-brightgreen.svg" alt="Node"/>
<img src="https://img.shields.io/badge/NestJS-11.x-red.svg" alt="NestJS"/>
Expand Down
2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dashboard",
"private": true,
"version": "0.4.1",
"version": "0.4.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "بدء",
"stop": "إيقاف",
"reconnect": "إعادة الاتصال",
"delete": "حذف"
"delete": "حذف",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "الجلسة جاهزة",
Expand All @@ -241,6 +242,16 @@
"sessionId": "معرّف الجلسة",
"lastActive": "آخر نشاط",
"error": "خطأ"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "فشل الحفظ"
}
}
}
}
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "Start",
"stop": "Stop",
"reconnect": "Reconnect",
"delete": "Delete"
"delete": "Delete",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "Session Ready",
Expand All @@ -241,6 +242,16 @@
"sessionId": "Session ID",
"lastActive": "Last Active",
"error": "Error"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "Save Failed"
}
}
}
}
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "Iniciar",
"stop": "Detener",
"reconnect": "Reconectar",
"delete": "Eliminar"
"delete": "Eliminar",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "Sesión lista",
Expand All @@ -241,6 +242,16 @@
"sessionId": "ID de sesión",
"lastActive": "Última actividad",
"error": "Error"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "Error al guardar"
}
}
}
}
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "Démarrer",
"stop": "Arrêter",
"reconnect": "Reconnecter",
"delete": "Supprimer"
"delete": "Supprimer",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "Session prête",
Expand All @@ -241,6 +242,16 @@
"sessionId": "ID de session",
"lastActive": "Dernière activité",
"error": "Erreur"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "Échec de l'enregistrement"
}
}
}
}
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "הפעלה",
"stop": "עצירה",
"reconnect": "התחברות מחדש",
"delete": "מחיקה"
"delete": "מחיקה",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "ה-Session מוכן",
Expand All @@ -241,6 +242,16 @@
"sessionId": "מזהה Session",
"lastActive": "פעילות אחרונה",
"error": "שגיאה"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "השמירה נכשלה"
}
}
}
}
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "Avvia",
"stop": "Ferma",
"reconnect": "Riconnetti",
"delete": "Elimina"
"delete": "Elimina",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "Sessione Pronta",
Expand All @@ -241,6 +242,16 @@
"sessionId": "ID Sessione",
"lastActive": "Ultima Attività",
"error": "Errore"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "Salvataggio non riuscito"
}
}
}
}
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/te.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "ప్రారంభించు",
"stop": "ఆపివేయి",
"reconnect": "మళ్లీ కనెక్ట్ చేయి",
"delete": "తొలగించు"
"delete": "తొలగించు",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "సెషన్ సిద్ధంగా ఉంది",
Expand All @@ -241,6 +242,16 @@
"sessionId": "సెషన్ ID",
"lastActive": "చివరిసారి యాక్టివ్",
"error": "లోపం"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "సేవ్ చేయడం విఫలమైంది"
}
}
}
}
15 changes: 13 additions & 2 deletions dashboard/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@
"start": "启动",
"stop": "停止",
"reconnect": "重新连接",
"delete": "删除"
"delete": "删除",
"killStuck": "Kill Stuck"
},
"toasts": {
"readyTitle": "会话就绪",
Expand All @@ -241,6 +242,16 @@
"sessionId": "会话 ID",
"lastActive": "上次活跃",
"error": "错误"
},
"forceKill": {
"title": "Kill Stuck Session",
"message": "Are you sure you want to force-kill session \"{{name}}\"?",
"warning": "This will forcefully terminate the WhatsApp browser process. The session may need to be re-scanned on restart.",
"confirm": "Kill Session",
"successTitle": "Session Killed",
"success": "Session has been force-killed. You can restart it now.",
"failedTitle": "Force-Kill Failed",
"failed": "Failed to force-kill the session."
}
},
"webhooks": {
Expand Down Expand Up @@ -605,4 +616,4 @@
"saveFailed": "保存失败"
}
}
}
}
Loading
Loading