204 lines
8.3 KiB
Markdown
204 lines
8.3 KiB
Markdown
# 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:
|
|
```js
|
|
{ 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 `Uninstall`ed 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)
|