Skip to the content.

Security model

← Back to index

Plugins are not sandboxed

A plugin runs inside the VRCNext page with the full authority of that page: the user’s logged-in VRChat session, their Discord webhook URLs, their settings, and the hundreds of backend actions VRCNext exposes. It is compiled into the same bundle as the host. There is no iframe, no realm, no process boundary.

Installing a plugin is equivalent to running a program from that repository. The system makes what a plugin does declared and confirmed; it does not contain a plugin that is determined to misbehave, and saying otherwise would invite users to install plugins they have not vetted.

What the system actually does

Real, verifiable controls. Each one narrows how a plugin gets in or what it does without asking; none of them holds against a malicious plugin the user has already enabled.

Control Detail
Nothing runs that the bridge did not compile The page runs one static bundle. It never evaluates code at runtime, never fetches a manifest, never loads a blob. The only way for code to reach the page is a git clone through the bridge.
Author signatures Every tree carries an Ed25519 plugin.sig over its files, bound to the plugin id. A plugin is pinned to the key it was installed under; an unknown key, and any later change of key, is confirmed natively and separately. See Signing.
Native confirmation Install, update and uninstall are confirmed outside the page — a desktop notification with Confirm/Deny on Linux, a message box on Windows — because the page cannot be trusted to confirm code being added to itself. No channel, no answer within two minutes: refused.
Transport https:// only, cloned with pure-Rust git (no shell, no system git), depth 1, 120 s deadline. Symlinks in the clone refuse the install.
Manifest validation plugin.json is parsed by the bridge at install and update, and again by the host at boot, through the same parser. Unknown fields and unknown permission names are errors.
Source policy Every source file is scanned before compiling; eval, window, fetch, storage, sockets, dynamic import and friends refuse the install, called or merely referenced, with file, line and rule. See Source policy.
Shape policy Before those names, source shaped so a reviewer cannot read it is refused: minified or packed files, runs of \xNN escapes, _0x-mangled identifiers, long high-entropy blobs, zero-width and bidirectional-override characters. A name-based scan is worthless on code nobody could read.
Declared ceiling Every capability that reaches outside the plugin’s own panels needs a category from permissions. Enabling shows each one with a risk tone; an undeclared category throws PermissionError and is never asked about.
First-use prompts Inside a category, each concrete target — a host, an action name, an event, a bridge method — is confirmed the first time, with the exact payload in the details. See Permissions.
The only fetch ctx.http.fetch asks about each host, forces the plugin’s abort signal, and is the only HTTP a plugin can do. Through the bridge it follows no redirects and reaches only public addresses, so an approved host cannot bounce a request onto this machine or its network. See How ctx.http.fetch travels.
Checksum-pinned compiler The bridge refuses to spawn esbuild unless its SHA-256 matches the checksum written at install; arguments are fixed and nothing from a plugin reaches them.
Paired, loopback-only daemon The bridge listens on loopback with no override, refuses non-loopback origins, and every socket must present the pairing token in its first frame.
Identity and version plugin.id in main.ts must equal the manifest’s; apiVersion is enforced before activation.
Reversibility Disabling disposes listeners, timers, UI, routes and menu contributions; a throwing listener, provider or route is logged and skipped, not fatal.

What it does not do

Guidance for users

Guidance for plugin authors

← Publishing & updates · Limitations →