Files
rmyndharis--openwa/docs/23-plugin-sandboxing.md
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:24:08 +08:00

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 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.

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