Skip to content

Plugin Sandboxing

Overview

Implementations that support plugins MUST isolate plugin execution to prevent malicious or buggy plugins from compromising the host system.

Threat Model

Plugins are untrusted code that may:

  • Access the filesystem beyond the ledger
  • Make network requests
  • Execute arbitrary system commands
  • Consume excessive resources
  • Leak sensitive financial data

Sandbox Requirements

Isolation Levels

LevelDescriptionUse Case
NoneFull host accessTrusted, audited plugins only
ProcessSeparate process, limited IPCNative plugins
ContainerOS-level isolationUntrusted plugins
WebAssemblyMemory-safe sandboxRecommended default

WASM provides strong isolation by design:

+---------------------------------------------+
|                Host Process                 |
|  +---------------------------------------+  |
|  |           WASM Runtime                |  |
|  |  +---------------------------------+  |  |
|  |  |         Plugin Module           |  |  |
|  |  |  - Linear memory (isolated)     |  |  |
|  |  |  - No direct syscalls           |  |  |
|  |  |  - Imported functions only      |  |  |
|  |  +---------------------------------+  |  |
|  +---------------------------------------+  |
+---------------------------------------------+

WASM Capabilities

CapabilityDefaultCan Enable
Memory accessOwn linear memory onlyNo
File systemNoneVia WASI (limited)
NetworkNoneNo
System callsNoneNo
Host functionsExplicitly importedYes

Implementation

WASM Runtime Selection

RuntimeLanguageFeatures
WasmtimeRustFull WASI, async
WasmerRustWASI, multiple backends
wasm3CLightweight, embedded
V8C++Browser compatible

Host-Plugin Interface

rust
// Host provides these functions to plugins
#[link(wasm_import_module = "beancount")]
extern "C" {
    // Read-only access to directives
    fn get_directive_count() -> u32;
    fn get_directive(index: u32) -> DirectiveRef;

    // Plugin output
    fn emit_directive(ptr: *const u8, len: u32);
    fn emit_error(ptr: *const u8, len: u32);

    // No filesystem, network, or other capabilities
}

Memory Limits

rust
// Configure WASM instance limits
let engine = Engine::new(&Config::new()
    .max_memory_size(64 * 1024 * 1024)  // 64 MB max
    .max_table_elements(10000)
    .max_instances(1)
)?;

Native Plugin Sandbox

For native (non-WASM) plugins:

Process Isolation

+-----------------+     IPC      +-----------------+
|   Main Process  |<------------>|  Plugin Process |
|   (privileged)  |              |  (sandboxed)    |
+-----------------+              +-----------------+

Linux Sandboxing

rust
use seccomp::*;

fn sandbox_plugin() -> Result<()> {
    // Allow only safe syscalls
    let filter = SeccompFilter::new(vec![
        allow_syscall(Syscall::read),
        allow_syscall(Syscall::write),  // stdout/stderr only
        allow_syscall(Syscall::mmap),
        allow_syscall(Syscall::exit_group),
        // Block everything else
    ])?;
    filter.load()?;
    Ok(())
}

macOS Sandboxing

c
// sandbox-exec profile
(version 1)
(deny default)
(allow process-exec)
(allow file-read-data (subpath "/path/to/ledger"))
(deny network*)
(deny file-write*)

Windows Sandboxing

rust
// Use AppContainer or restricted token
use windows::Win32::Security::*;

fn create_restricted_token() -> HANDLE {
    // Remove all privileges
    // Deny admin SIDs
    // Add restricted SIDs
}

Resource Limits

Execution Time

rust
// Timeout plugin execution
let result = tokio::time::timeout(
    Duration::from_secs(30),
    plugin.run(directives)
).await?;

Memory Usage

rust
// Monitor and limit memory
if plugin_memory_usage() > MAX_PLUGIN_MEMORY {
    plugin.terminate();
    return Err(PluginError::MemoryExceeded);
}

CPU Usage

rust
// Limit CPU time (WASM fuel metering)
let mut store = Store::new(&engine, ());
store.add_fuel(1_000_000)?;  // 1M instructions max

match instance.call(&mut store, "run", &[]) {
    Err(e) if e.is::<Trap>() => {
        // Out of fuel - plugin took too long
    }
    ...
}

Data Isolation

Read-Only Input

Plugins MUST NOT be able to modify input directives:

rust
// Pass immutable references
fn run_plugin(plugin: &Plugin, directives: &[Directive]) -> Vec<Directive> {
    // Plugin receives read-only view
    // Returns new directives (additions/modifications)
}

Output Validation

Validate all plugin output:

rust
fn validate_plugin_output(output: &[Directive]) -> Result<()> {
    for directive in output {
        // Verify directive is well-formed
        // Check for injection attempts
        // Validate metadata
    }
    Ok(())
}

Security Modes

ModeWASMNativeTrust Level
StrictYes, full sandboxRejectUntrusted
StandardYes, full sandboxProcess isolatedSemi-trusted
PermissiveYes, minimal limitsAllowedTrusted

Configuration

beancount
; Plugin security settings
option "plugin_sandbox" "strict"     ; Enforce WASM sandbox
option "plugin_timeout" "30"         ; Seconds
option "plugin_memory_limit" "64MB"  ; Max memory per plugin

Error Handling

error: Plugin execution blocked by sandbox
  --> main.beancount:1:1
  |
1 | plugin "malicious_plugin"
  | ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = reason: attempted filesystem access outside allowed paths
  = path: /etc/passwd
  = hint: plugins cannot access files outside the ledger directory

Recommendations

  1. Default to WASM for all plugins
  2. Reject native plugins unless explicitly allowed
  3. Set resource limits (time, memory, CPU)
  4. Validate all output from plugins
  5. Log plugin activity for auditing
  6. Use process isolation for native plugins if WASM not available