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 manipulationtable— table operationsmath— mathematical functionstype,tostring,tonumber— type operationspcall,xpcall— protected callserror,assert— error handlingselect,unpack— argument manipulationpairs,ipairs,next— iterationcollectgarbage— 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):
- funcName validation: Only
[a-zA-Z_][a-zA-Z0-9_]*segments separated by dots (max 2 dots, 3 segments) - No string-based code generation: Functions are resolved via
_Gtable walk, notDoString - Direct PCall: Arguments are pushed as
lua.LValuedirectly, not serialized to Lua code - 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
- Source verification: Messages are only accepted from the specific
iframeEl.contentWindow(not just anysource: 'calendar-plugin') - Type checking:
msg.source,msg.actionmust be strings - sandbox attribute: iframe uses
sandbox="allow-scripts allow-same-origin"(no forms, no popups) - 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
Installon already-installed plugin → error (no duplicate config entries)Uninstallon non-installed plugin → errorEnableon non-installed plugin → errorDisableon already-disabled plugin → no-op (error, but harmless)ReloadPlugins→ stops schedulers, calls shutdown hooks, closes VMs, re-discovers
ReloadPlugins Cleanup Order
StopSchedulers()— stops all background tasks, setsscheduler = nilCallShutdownHooks()— callson_shutdownfor all active pluginsCloseRuntimes()— closes all Lua VMs, setsvm = nilDiscover()— re-scans plugins directorySyncConfig()— applies installed/enabled state from configInitRuntimes()— creates new VMs and schedulersCallInitHooks()— callson_initfor all active pluginsStartSchedulers()— 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 blockedTestLuaVM_Sandbox_SafeLibs— 9 safe libraries availableTestLuaVM_Sandbox_GlobalsRemoved— 11 dangerous globals nilTestLuaVM_Sandbox_OsRestricted— 6 os.* functions blocked, 5 safe ones availableTestLuaVM_Sandbox_IoBlocked— io library not openedTestLuaVM_Sandbox_DebugBlocked— debug library not openedTestLuaVM_Sandbox_CoroutineBlocked— coroutine library not openedTestLuaVM_Sandbox_Package*Blocked— 9 package.* fields blockedTestLuaVM_Sandbox_PrintIsRedirected— print goes to Go logTestLuaVM_DoStringTimeout— infinite loops killed by timeoutTestLuaVM_Isolation— plugins cannot see each other's globalsTestLuaVM_ClosedVM— operations on closed VM return errorsTestLuaVM_DoString_InjectionAttempt— dangerous APIs return nilTestPluginManager_InstallIdempotent— no duplicate config entriesTestPluginManager_DisablePreservesData— Disable keeps dataTestPluginManager_UninstallRemovesFromConfig— Uninstall cleans configTestPluginManager_ReloadPlugins_Cleanup— full cleanup on reloadTestCallPluginFunction_Validation— funcName validation (5 valid, 10 invalid cases)