4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
84 lines
5.1 KiB
Markdown
84 lines
5.1 KiB
Markdown
# 23 — Plugin Sandboxing
|
|
|
|
OpenWA runs **untrusted plugins** (anything loaded from the plugins directory) in an isolated worker
|
|
thread, separate from first-party built-ins (engines, bundled extensions) which run in-process. This
|
|
page describes the security model honestly — what the sandbox guarantees and, just as important, what
|
|
it does not — and what changes for plugin authors.
|
|
|
|
## Trust tiers
|
|
|
|
| Tier | Examples | Runs | Capabilities |
|
|
|------|----------|------|--------------|
|
|
| **Built-in (trusted)** | engines (whatsapp-web.js, baileys), bundled extensions (auto-reply, translation) | in-process | direct, full speed |
|
|
| **Untrusted** | anything in the plugins directory | in a `worker_thread` | only via the host-validated bridge |
|
|
|
|
The loader routes by tier automatically: a plugin registered programmatically is built-in; one loaded
|
|
from disk is untrusted and sandboxed.
|
|
|
|
## What the sandbox guarantees
|
|
|
|
- **No host-object access.** The worker runs in its own V8 context. It receives no reference to the
|
|
loader, the engine, the database, `MessageService`, or any host singleton. Its only channel out is
|
|
a `MessagePort`.
|
|
- **Capability mediation.** A plugin can touch WhatsApp / the database **only** through
|
|
`ctx.messages.*` / `ctx.engine.*` / `ctx.storage.*`, which round-trip to the host. The host runs
|
|
each call through the same permission + session-scope checks an in-process plugin gets
|
|
(`assertPermission` / declared `sessions`), so a sandboxed plugin can never exceed its declared
|
|
manifest permissions. Verbs are allowlisted — a worker cannot invoke an arbitrary host method.
|
|
- **Hook safety.** Hook handlers run in the worker and are dispatched with a **time budget**
|
|
(`SANDBOX_HOOK_TIMEOUT_MS`, default 5s). A slow or wedged handler is skipped (`continue: true`) so
|
|
it can never stall the host's hook chain.
|
|
- **Resource & runaway containment.** Each worker has a heap cap (`maxOldGenerationSizeMb`, default
|
|
256). An OOM terminates the worker, not the host. A wedged **lifecycle** call (load/unload) times out
|
|
and tears the worker down, and a crash rejects its in-flight calls — the host survives either way. A
|
|
runaway **hook** handler (e.g. an infinite synchronous loop) is skipped on the hook timeout so it can't
|
|
stall the host's hook chain, but the worker keeps running it (pegging a core) until the plugin is
|
|
reloaded or hits the heap cap — it is contained to its own thread, not instantly force-killed.
|
|
|
|
## What the sandbox does NOT guarantee
|
|
|
|
> A `worker_thread` is a separate V8 context **in the same OS process, under the same user**. It is
|
|
> not an OS-level sandbox.
|
|
|
|
A worker still has access to Node built-ins — `require('fs')`, `process`, network sockets — and runs
|
|
as the same uid as OpenWA. The sandbox therefore does **not**, by itself, stop a malicious plugin
|
|
from reading files the OpenWA process can read or making outbound network connections. It protects
|
|
the *integrity* of the host (no host-object compromise, contained faults, mediated capabilities) — not
|
|
the *confidentiality* of the host filesystem against deliberate Node-builtin abuse.
|
|
|
|
For genuinely untrusted, third-party plugins, combine the sandbox with **OS-level containment**:
|
|
|
|
- **Run OpenWA in its container** (the shipped Docker image already runs read-only rootfs, non-root,
|
|
and `cap_drop: ALL`), which bounds what any plugin's `fs`/network access can reach.
|
|
- Until a marketplace exists, the standing guidance remains: **install only plugins you trust.**
|
|
|
|
A stronger isolation variant (child process with Node's permission model, or an `isolated-vm`) is a
|
|
possible future enhancement for maximum-hostility deployments; the transport is already abstracted
|
|
behind a channel interface so it can slot in without touching plugin code.
|
|
|
|
## What changes for plugin authors
|
|
|
|
Sandboxed plugins keep the same `IPlugin` shape (`onLoad`/`onEnable`/`onDisable`/`onUnload`,
|
|
`ctx.messages`, `ctx.engine`, `ctx.storage`, `ctx.registerHook`), with these rules:
|
|
|
|
1. **Capability calls are remote.** They were already `async`; they now genuinely cross a thread
|
|
boundary. Nothing to change in usage.
|
|
2. **Only serializable data crosses.** Hook payloads and capability args/results must be
|
|
structured-clone-safe — plain objects, arrays, primitives, `Date`, typed arrays. **No functions,
|
|
no class instances with methods, no live references.**
|
|
3. **No ambient host access.** `require('fs')`/`process` etc. still exist in the worker but must not
|
|
be relied on as a capability — anything the plugin legitimately needs should be a declared
|
|
capability, and OS containment may block direct access.
|
|
4. **Declare your permissions.** A capability call is denied unless the manifest declares it
|
|
(`messages:send`, `engine:read`). See [19 — Plugin Architecture](./19-plugin-architecture.md).
|
|
|
|
This is a **breaking change for third-party plugin authoring** and ships in a minor release. Built-in
|
|
plugins are unaffected (they remain in-process).
|
|
|
|
## Configuration
|
|
|
|
| Env / constant | Default | Purpose |
|
|
|----------------|---------|---------|
|
|
| `maxOldGenerationSizeMb` (worker) | 256 | per-plugin heap cap; OOM kills the worker, not the host |
|
|
| hook handler timeout | 5000 ms | budget before a sandboxed hook handler is skipped |
|