verstak/docs/plugins-security.md

8.3 KiB

Plugin Security Model

Overview

Verstak's Lua plugin system is designed with a sandbox-first approach. Plugins run in isolated Lua 5.1 states with restricted access to the host system. This document defines the security boundaries.

Lua Sandbox

Blocked Global Functions

The following Lua globals are set to nil in every plugin VM:

Function Reason
dofile Arbitrary file read/execute
loadfile Arbitrary file read
require Module loading (no filesystem access)
module Module system (deprecated, unsafe)
load Arbitrary code execution from string
rawequal Bypass metatable protections
rawget Bypass metatable protections
rawset Bypass metatable protections
rawlen Bypass metatable protections
setfenv Environment manipulation
getfenv Environment manipulation

Blocked Libraries

Library Status Notes
io Not opened No file I/O
debug Not opened No debug introspection
coroutine Not opened No coroutine creation
os Restricted Only clock, date, difftime, time, tmpname
package Restricted Only seeall is available; loadlib, preload, loaders, loaded, path, cpath, config, searchpath are blocked

Restricted os.* Functions

Function Status Reason
os.clock Allowed Safe timing
os.date Allowed Safe date formatting
os.difftime Allowed Safe time diff
os.time Allowed Safe timestamp
os.tmpname Allowed Safe temp name
os.execute Blocked Shell command execution
os.exit Blocked Process termination
os.remove Blocked File deletion
os.rename Blocked File renaming
os.setlocale Blocked Locale manipulation
os.getenv Blocked Environment variable access

Safe Libraries (Available)

  • string — string manipulation
  • table — table operations
  • math — mathematical functions
  • type, tostring, tonumber — type operations
  • pcall, xpcall — protected calls
  • error, assert — error handling
  • select, unpack — argument manipulation
  • pairs, ipairs, next — iteration
  • collectgarbage — GC control

Print Redirection

print() is redirected to Go's log.Printf with the [lua] prefix. Plugins cannot write to stdout directly.

Plugin Function Call Security

CallPluginFunction Validation

When the frontend calls a plugin function via CallPluginFunction(pluginName, funcName, paramsJSON):

  1. funcName validation: Only [a-zA-Z_][a-zA-Z0-9_]* segments separated by dots (max 2 dots, 3 segments)
  2. No string-based code generation: Functions are resolved via _G table walk, not DoString
  3. Direct PCall: Arguments are pushed as lua.LValue directly, not serialized to Lua code
  4. Plugin must be active: Only enabled plugins can receive calls

Blocked funcName Patterns

❌ "func;os.execute('id')"  — semicolon injection
❌ "func..name"             — empty segment
❌ "func[name]"             — bracket injection
❌ "func()"                 — parentheses
❌ "func name"              — space injection
❌ "func-name"              — special chars
❌ "a.b.c.d"                — too deep (>3 segments)
❌ "123func"                — starts with number

iframe Bridge Security

PluginPage.svelte ↔ iframe Communication

  1. Source verification: Messages are only accepted from the specific iframeEl.contentWindow (not just any source: 'calendar-plugin')
  2. Type checking: msg.source, msg.action must be strings
  3. sandbox attribute: iframe uses sandbox="allow-scripts allow-same-origin" (no forms, no popups)
  4. srcdoc: iframe content is injected as srcdoc (same origin, no external network)

Message Protocol

All messages must have:

{ source: 'calendar-plugin', action: 'ready' | 'get-events' | 'create-event' | ... }

Messages from iframe to Svelte are validated:

  • e.source === iframeEl.contentWindow (origin check)
  • msg.source === 'calendar-plugin' (source check)
  • typeof msg.action === 'string' (type check)

Plugin Lifecycle Security

State Separation

State Description Data Persistence
Discovered Plugin found on disk None
Installed on_install hook ran, tables created DB tables exist
Enabled Plugin is active, Lua VM running Full access
Active Runtime is initialized Full access
Disabled Plugin deactivated, data preserved DB tables exist
Uninstalled on_uninstall hook ran, tables dropped Data cleaned

Idempotency

  • Install on already-installed plugin → error (no duplicate config entries)
  • Uninstall on non-installed plugin → error
  • Enable on non-installed plugin → error
  • Disable on already-disabled plugin → no-op (error, but harmless)
  • ReloadPlugins → stops schedulers, calls shutdown hooks, closes VMs, re-discovers

ReloadPlugins Cleanup Order

  1. StopSchedulers() — stops all background tasks, sets scheduler = nil
  2. CallShutdownHooks() — calls on_shutdown for all active plugins
  3. CloseRuntimes() — closes all Lua VMs, sets vm = nil
  4. Discover() — re-scans plugins directory
  5. SyncConfig() — applies installed/enabled state from config
  6. InitRuntimes() — creates new VMs and schedulers
  7. CallInitHooks() — calls on_init for all active plugins
  8. StartSchedulers() — starts background tasks

Network Access

Plugins have no direct network access. All HTTP/network operations must go through the verstak.http.* API (registered by the core), which:

  • Uses Go's net/http (not Lua socket)
  • Can be rate-limited by the core
  • Goes through the proxy if configured

Filesystem Access

Plugins have no direct filesystem access. All file operations must go through:

  • verstak.db.* — SQLite queries (plugin's own DB only)
  • verstak.node.* — Node CRUD (via core services)
  • verstak.files.* — File operations (via core services)

The io library is not opened. os.remove, os.rename are blocked.

Database Isolation

Each plugin gets its own SQLite database at .verstak/plugins/<name>/data/. Plugins cannot access:

  • The main Verstak database
  • Other plugins' databases
  • System databases

Calendar Plugin (Reference)

The Calendar plugin in contrib/plugins/calendar/ is a reference implementation, not a core feature:

  • It is not loaded by default
  • It requires explicit Install + Enable
  • Its tables are created only during on_install
  • It can be fully Uninstalled without affecting core functionality
  • It does not add calendar UI to any core views
  • It demonstrates the full plugin API: verstak.db.*, verstak.config.*, verstak.state.*, verstak.node.*, verstak.activity.*, verstak.schedule.*, verstak.http.*

Security Tests

All security properties are verified by tests in internal/core/plugins/security_test.go:

  • TestLuaVM_Sandbox_Comprehensive — 12 dangerous APIs blocked
  • TestLuaVM_Sandbox_SafeLibs — 9 safe libraries available
  • TestLuaVM_Sandbox_GlobalsRemoved — 11 dangerous globals nil
  • TestLuaVM_Sandbox_OsRestricted — 6 os.* functions blocked, 5 safe ones available
  • TestLuaVM_Sandbox_IoBlocked — io library not opened
  • TestLuaVM_Sandbox_DebugBlocked — debug library not opened
  • TestLuaVM_Sandbox_CoroutineBlocked — coroutine library not opened
  • TestLuaVM_Sandbox_Package*Blocked — 9 package.* fields blocked
  • TestLuaVM_Sandbox_PrintIsRedirected — print goes to Go log
  • TestLuaVM_DoStringTimeout — infinite loops killed by timeout
  • TestLuaVM_Isolation — plugins cannot see each other's globals
  • TestLuaVM_ClosedVM — operations on closed VM return errors
  • TestLuaVM_DoString_InjectionAttempt — dangerous APIs return nil
  • TestPluginManager_InstallIdempotent — no duplicate config entries
  • TestPluginManager_DisablePreservesData — Disable keeps data
  • TestPluginManager_UninstallRemovesFromConfig — Uninstall cleans config
  • TestPluginManager_ReloadPlugins_Cleanup — full cleanup on reload
  • TestCallPluginFunction_Validation — funcName validation (5 valid, 10 invalid cases)