Bulwark Webmail: The Plugin Sandbox Accepts Code From Any Foreign Site
The sandbox routes are reachable without login, carry unsafe-eval and bind to the first window that posts to them, without checking its origin
Advisory ID: TP-2026-103
Product: Bulwark Webmail (web interface for the Stalwart mail server, Next.js)
Vulnerability type: Missing origin validation (CWE-346)
CVE: requested on 19 September 2026 (GitHub CNA), assignment pending
CVSS 3.1: 8.8 (High) · CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Affected versions: >= 1.7.0, <= 1.9.2
Vendor advisory: GHSA-96cx-gx36-3g79
Fixed in: 1.10.0
Reported: 16 September 2026
Summary
Bulwark runs extensions in two dedicated documents that are reachable as top-level pages and carry unsafe-eval. Their message handler remembers the first window that posts to it and never compares that window's origin against its own. Any foreign site therefore opens one of those routes, posts its init message before the real host does, and the code string in that message reaches new Function inside the webmail origin. The extension trust chain is never consulted, because the signature and hash checks live in modules the sandbox runtime does not import at all. Disabling plugins does not help, since the routes still answer successfully.
Root cause
The sandbox runtime accepts the first inbound message from any window and stores that window's reference as the host, without checking its origin (lib/plugin-sandbox/runtime.tsx:628-636). The routes /plugin-sandbox and /plugin-sandbox-privileged are top-level documents requiring no login and carry unsafe-eval, so the supplied string is executed through new Function. The extension trust chain stays untouched: the Ed25519 signature is checked where the store downloads a bundle, and the hash and tier are checked by the loader, while the runtime imports none of those modules. The plugin setting defaults to off and offers no protection, since the routes still answer successfully and without a cookie. The handler is byte-identical across all fourteen releases from 1.7.0 to 1.9.2; versions up to 1.6.7 ship no sandbox at all.
Proof of Concept
# any foreign site the signed-in victim visits:
const w = window.open('https://webmail.example/plugin-sandbox');
// post the init message before the real host does:
w.postMessage({type: 'init', code: '<the attacker's javascript>'}, '*');
// the handler stores the first window as the host, never checks its
// origin, and new Function runs the code in the webmail origin.
One click by the signed-in victim on the foreign site suffices. The /plugin-sandbox route alone is enough; the privileged variant exists from 1.7.6 and behaves the same. The code runs without a login too, only the impact requires a signed-in session.
Impact
- Execution of arbitrary JavaScript in the webmail origin by a foreign site after one click by the victim.
- Disclosure of the session and control of the JMAP passthrough in the victim's name.
- No account on the instance needed; the attack starts on any foreign site.
- No protection from disabling plugins, since the routes answer regardless.
References
Is Something Like This in Your Software?
Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.
