5.1 KiB
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 aMessagePort. - 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/ declaredsessions), 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_threadis 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'sfs/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:
- Capability calls are remote. They were already
async; they now genuinely cross a thread boundary. Nothing to change in usage. - 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. - No ambient host access.
require('fs')/processetc. 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. - Declare your permissions. A capability call is denied unless the manifest declares it
(
messages:send,engine:read). See 19 — Plugin Architecture.
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 |