馃殌 Feature Proposal
Isolate plugin JavaScript (UI extensions and EXPLAIN parsers) from the main app, so a plugin can only reach the backend through an explicit, limited API.
Motivation
Plugin bundles currently run with new Function(...) directly in the main window:
src/contexts/PluginSlotProvider.tsx (UI slots)
src/utils/pluginExplainLoader.ts (evaluateBundle)
They share the app's JS context, so they can call any Tauri command the app can, for example export_connections_payload, which returns every saved password, SSH secret and connection URI (include_secrets defaults to true in src-tauri/src/commands.rs). tauri.conf.json sets "csp": null, so nothing stops a fetch to an external host afterwards.
Registry signing (src-tauri/src/plugins/integrity.rs) proves a bundle is the one the registry published. It does not limit what that bundle can do, so a malicious or compromised plugin can read all stored credentials without the user being asked.
Note: native driver plugins already run as local processes with the user's privileges, so this is defence in depth for the JS side, not a full plugin trust model.
Where to start
- CSP: replace
"csp": null with a real policy. List what the app needs today (Monaco workers, ipc: / http://ipc.localhost, inline styles, AI providers / registry hosts if called from the frontend) and keep connect-src as narrow as possible.
export_connections_payload: make include_secrets default to false. The only caller (src/pages/Connections.tsx, handleExport) already passes it explicitly. On its own this does not stop a plugin, which can pass true, so also move secret export behind a user confirmation handled in the backend (e.g. a native dialog).
- Sandbox: run plugin UI in a sandboxed
<iframe> (or a separate webview with its own Tauri capability set) and expose __TABULARIS_API__ / __TABULARIS_EXPLAIN__ over postMessage with an allow-list. Tauri capabilities apply per window/webview, so they only help once plugins live in their own webview.
- Write a short design note in this issue before starting the sandbox part: it changes the plugin API contract and needs a migration path for existing plugins.
Acceptance criteria
- A CSP is set and the app (editor, ER diagram, visual explain, AI panel, plugin registry) still works.
export_connections_payload without arguments returns no secrets; exporting with secrets requires an explicit user confirmation.
- Plugin JS cannot call Tauri
invoke directly; the allowed plugin API is documented.
- Existing first-party plugins keep working, or the needed migration is documented.
Contributor notes
- Follow the repo rules in
.rules/ (TypeScript, React, Rust, testing).
- Any new UI string needs a key in every locale under
src/i18n/locales/.
- Run
pnpm lint, pnpm typecheck, pnpm test and cargo test before opening the PR.
- Questions? Ask here or on Discord.
馃殌 Feature Proposal
Isolate plugin JavaScript (UI extensions and EXPLAIN parsers) from the main app, so a plugin can only reach the backend through an explicit, limited API.
Motivation
Plugin bundles currently run with
new Function(...)directly in the main window:src/contexts/PluginSlotProvider.tsx(UI slots)src/utils/pluginExplainLoader.ts(evaluateBundle)They share the app's JS context, so they can call any Tauri command the app can, for example
export_connections_payload, which returns every saved password, SSH secret and connection URI (include_secretsdefaults totrueinsrc-tauri/src/commands.rs).tauri.conf.jsonsets"csp": null, so nothing stops afetchto an external host afterwards.Registry signing (
src-tauri/src/plugins/integrity.rs) proves a bundle is the one the registry published. It does not limit what that bundle can do, so a malicious or compromised plugin can read all stored credentials without the user being asked.Note: native driver plugins already run as local processes with the user's privileges, so this is defence in depth for the JS side, not a full plugin trust model.
Where to start
"csp": nullwith a real policy. List what the app needs today (Monaco workers,ipc:/http://ipc.localhost, inline styles, AI providers / registry hosts if called from the frontend) and keepconnect-srcas narrow as possible.export_connections_payload: makeinclude_secretsdefault tofalse. The only caller (src/pages/Connections.tsx,handleExport) already passes it explicitly. On its own this does not stop a plugin, which can passtrue, so also move secret export behind a user confirmation handled in the backend (e.g. a native dialog).<iframe>(or a separate webview with its own Tauri capability set) and expose__TABULARIS_API__/__TABULARIS_EXPLAIN__overpostMessagewith an allow-list. Tauri capabilities apply per window/webview, so they only help once plugins live in their own webview.Acceptance criteria
export_connections_payloadwithout arguments returns no secrets; exporting with secrets requires an explicit user confirmation.invokedirectly; the allowed plugin API is documented.Contributor notes
.rules/(TypeScript, React, Rust, testing).src/i18n/locales/.pnpm lint,pnpm typecheck,pnpm testandcargo testbefore opening the PR.