Security Guide
Overview
Scriptling’s security boundary depends entirely on how it is configured. You are responsible for registering only the capabilities your scripts need and for applying process-level isolation where appropriate.
Bare Embedding vs CLI Defaults
A bare scriptling.New() interpreter starts without standard or extended libraries; the embedding host chooses what to register. It still provides the language runtime and import mechanism, but an import succeeds only after the host registers a library or configures a loader.
The scriptling CLI is intentionally more permissive. Unless disabled, its setup registers the standard libraries plus extended capabilities including requests, os, and subprocess:
requestscan reach the network unless--network-policyrestricts it.oscan access the filesystem unless--allowed-pathsrestricts its filesystem operations.os.getenv()reads the live process environment, andos.environ()exposes a snapshot of it. Filesystem allowlists do not filter environment variables.subprocesscan execute host commands and bypass library-level filesystem and network controls.
Scriptling runs in a memory-safe Go environment without C extensions and does not directly expose the Go runtime, but the CLI capabilities above still provide host access. Do not describe the default CLI as a no-filesystem, no-network, or no-environment sandbox.
Library Security
Pure Standard Libraries
These libraries do pure computation with no filesystem, network, or process access, so embedding hosts can register them freely (the CLI includes them; a bare scriptling.New() does not): math, json, datetime, time, string, base64, html, re, random, statistics, textwrap, functools, itertools, collections, hashlib, hmac, and uuid. platform is read-only platform information, and urllib parses URLs without making network requests.
Scriptling-Specific Libraries
These libraries provide Scriptling-specific functionality. This is a capability inventory, not a list of libraries available in every runtime; embedding hosts register them explicitly and CLI availability varies by mode and build.
| Library | Security Considerations |
|---|---|
scriptling.ai |
NETWORK ACCESS - Makes HTTP requests to AI APIs |
scriptling.ai.agent |
NETWORK + CODE EXECUTION - Agentic AI with tool execution |
scriptling.mcp |
NETWORK ACCESS - MCP protocol communication |
scriptling.net.* |
NETWORK ACCESS - DNS, UDP/TCP, gossip, multicast, and WebSocket clients |
scriptling.messaging.* |
NETWORK + REMOTE INPUT - Bot clients receive events and send messages using script-supplied credentials |
scriptling.container |
INFRASTRUCTURE CONTROL - Manages Docker, Podman, and Apple Containers |
scriptling.nomad |
INFRASTRUCTURE CONTROL - Registers/stops jobs and manages Nomad resources |
scriptling.oauth |
NETWORK ACCESS - Signs in to identity providers and handles access tokens |
scriptling.plugin |
PROCESS OR NETWORK ACCESS - Starts executable peers or connects to plugin servers |
scriptling.provision.* |
FILESYSTEM + NETWORK ACCESS - Changes files and can download remote content |
scriptling.secret |
Host-controlled secret access; scripts see aliases, not provider credentials |
scriptling.console |
Console I/O, including interactive input |
scriptling.similarity and scriptling.toon are pure computation with no external access.
Runtime Libraries
These libraries provide server and concurrency functionality:
| Library | Security Considerations |
|---|---|
scriptling.runtime |
Background task execution (isolated and shared-env threads) |
scriptling.runtime.http |
HTTP SERVER - Registers HTTP routes and handlers |
scriptling.runtime.kv |
In-memory key-value store - safe but unbounded memory |
scriptling.runtime.sync |
Concurrency primitives - safe but can cause deadlocks |
scriptling.runtime.sandbox |
CODE EXECUTION - Executes code in isolated environments |
Extended Libraries (Require Explicit Registration)
These libraries extend functionality but require explicit registration:
| Library | Security Considerations |
|---|---|
requests |
NETWORK ACCESS - Allows HTTP/HTTPS requests to external URLs |
os |
FILE SYSTEM ACCESS - Controlled by allowed paths |
os.path |
FILE SYSTEM ACCESS - Path operations, controlled by allowed paths |
pathlib |
FILE SYSTEM ACCESS - Object-oriented paths, controlled by allowed paths |
glob |
FILE SYSTEM ACCESS - Pattern matching, controlled by allowed paths |
subprocess |
CRITICAL RISK - Allows arbitrary command execution |
sys |
SYSTEM ACCESS - Provides access to system internals |
secrets |
Cryptographically secure random generation |
logging |
File and console logging - may write to disk |
scriptling.wait_for |
Network/resource polling - may access network; file checks controlled by allowed paths |
scriptling.provision.fetch |
HTTP/HTTPS downloads and filesystem writes; insecure=True skips TLS verification |
yaml |
YAML parsing - safe but watch for large files |
toml |
TOML parsing - safe but watch for large files |
html.parser |
HTML parsing - safe but watch for large files |
Never Register in Untrusted Environments
Do NOT register these libraries when executing untrusted code:
subprocess- Allows arbitrary command executionsys- Provides access to system internals and environmentscriptling.runtime.sandbox- Can execute arbitrary codescriptling.ai.agent- Can execute AI-generated code with tools
File System Security
Restricting File Access
When registering filesystem libraries, you must specify allowed paths:
// Safe: Only allows access to specific directories
extlibs.RegisterOSLibrary(p, []string{
"/tmp/myapp/data",
"/home/user/documents",
})
extlibs.RegisterPathlibLibrary(p, []string{
"/tmp/myapp/data",
"/home/user/documents",
})
extlibs.RegisterTempfileLibrary(p, []string{"/tmp/myapp"})
extlibs.RegisterShutilLibrary(p, []string{
"/tmp/myapp/data",
"/home/user/documents",
})
// Dangerous: Allows access to entire file system
extlibs.RegisterOSLibrary(p, nil) // Nil = no restriction (full read/write access)
extlibs.RegisterOSLibrary(p, []string{}) // Empty = deny all (no paths allowed)All filesystem libraries (os, pathlib, fs, glob, tempfile, shutil, zipfile, tarfile, scriptling.grep, scriptling.find, scriptling.sed) accept allowedPaths and enforce the same path traversal and symlink protections. A path is resolved the way the kernel resolves it when the file is opened: every symlink component is followed before any .. is applied, so a symlink inside an allowed directory cannot lead outside it — including a dangling symlink, one whose target does not exist yet (a script cannot plant allowed/link -> ../../etc/x and then create the file through it). os.symlink() likewise refuses to create a link whose target, resolved against the link’s own directory, falls outside the allowed set. Archive libraries (zipfile, tarfile) additionally block zip-slip / tar-slip attacks (path traversal via crafted entry names).
Path Traversal Protection
Scriptling’s file system libraries automatically prevent path traversal attacks:
# User tries to escape allowed directory
import os
import pathlib
allowed_path = "/tmp/myapp/data"
# Trying to access parent directories
os.read_file("/tmp/myapp/data/../../etc/passwd") # BLOCKED
os.read_file("/tmp/myapp/data/secrets.txt") # ALLOWED
path = pathlib.Path("/tmp/myapp/data/../../../etc/passwd") # BLOCKEDNetwork Security
Network-Enabled Libraries
These libraries can make network requests:
requests- HTTP client libraryscriptling.sql- MySQL, MariaDB and PostgreSQL client (see Database Drivers)scriptling.valkey- Valkey/Redis client (see Database Drivers)scriptling.ai- AI API clientscriptling.ai.agent- Agentic AI with tool executionscriptling.mcp- MCP protocol clientscriptling.wait_for- Resource polling (may check network endpoints)scriptling.provision.fetch- HTTP/HTTPS downloads and optional zip unpacking
Disabling Network Access
To keep the sandbox network-free, don’t register network-enabled libraries:
// Safe: Only register standard libraries (no network access)
stdlib.RegisterAll(p)
// Unsafe: Registers network-enabled library
extlibs.RegisterRequestsLibrary(p)Network Policies
For scripts that should reach the internet but must never reach your private network, register a network policy. A policy governs the requests, scriptling.wait_for, scriptling.net.websocket, scriptling.net.unicast, scriptling.net.multicast, scriptling.net.gossip, scriptling.nomad, scriptling.oauth, scriptling.ai, scriptling.mcp, and scriptling.provision.fetch libraries, the Slack, Discord and Telegram messaging libraries, and the container endpoints a script passes to scriptling.container, and is enforced at connect time: the hostname is resolved through the configured DNS servers, every resolved address is checked against the policy, and the connection is made to the validated address directly. That closes the usual bypasses — DNS rebinding (the answer changing between check and connect), redirects to internal hosts, and IP-notation tricks.
With a policy active, loopback, link-local (including cloud metadata endpoints like 169.254.169.254), private, reserved (CGNAT 100.64.0.0/10, documentation and benchmarking ranges, NAT64/6to4 and other non-public ranges), unspecified, and multicast addresses are all blocked by default, as are URLs that name an IP directly. Host allow/deny lists, CIDR exceptions, https-only, and custom DNS servers grant exactly the access you intend — allowlisted hosts are trusted to resolve internally (still subject to deny_cidrs, and a link-local answer still needs allow_cidrs), and deny rules always win.
- CLI:
--network-policy=policy.toml— the full policy file reference is in the network policy guide. Combine it with--disable-lib subprocessin any mode so code cannot bypass the policy by shelling out. - Embedding: pass a
*netsecurity.Config(or load the same TOML file withnetsecurity.LoadConfig) when registering the governed libraries — theConfigoptions are documented in the library registration guide. No policy means no restrictions.
policy, err := netsecurity.LoadConfig("policy.toml")
if err != nil {
return err // an invalid policy must never degrade into an open one
}
extlibs.RegisterRequestsLibrary(p, policy)
extlibs.RegisterWaitForLibrary(p, policy)
extlibs.RegisterWebSocketLibrary(p, policy)
ai.Register(p, policy)
scriptlingmcp.Register(p, policy)The policy governs the libraries above, including ai.Client()’s remote_servers (the MCP servers it can attach for tool use) and mcp.Client()’s HTTP transport — a script cannot bypass the policy by reaching a blocked endpoint through the AI or MCP client instead of requests. Both scriptling.wait_for probes, http() and port(), honor the policy and raise a network policy: … error for a denied address instead of polling until the timeout. scriptling.net.unicast checks connect() and a UDP listener’s send_to() destinations; listen() is unaffected. scriptling.net.multicast refuses every group in join() unless allow_cidrs grants it. scriptling.net.gossip checks every outbound connection, including to peers learned from the cluster, and sends everything over TCP; with allow_hosts set, peers are limited to the listed names, and a peer given by IP must be granted by allow_cidrs or allow_ip_literals. The Slack, Discord and Telegram libraries also confine the local files send_file() (and Telegram rich-message images) read to --allowed-paths. They send every request and gateway connection through the policy, their platforms’ own hosts included (under a strict allow_hosts, list those hosts), and send the bot token only to the platform’s hosts. nomad.Client(addr) requests, including redirects and insecure=True clients, go through the same checks, as do TCP/HTTPS endpoints a script passes to container.Client(socket=...) (the operator’s configured container endpoints are trusted; a script-supplied Unix socket path must lie within --allowed-paths). Stdio-transport mcp.Client() instances launch a local subprocess, which the network policy does not govern; they are refused wherever the subprocess library is disabled. scriptling.provision.fetch downloads through the same policy as requests, and confines dest, provides and unpacked files to --allowed-paths. Keep any of these unregistered where scripts are untrusted and no policy is configured. HTTP clients an embedding application builds itself and plugin processes are outside the policy (a plugin only enforces what it is handed; see Database Drivers).
Database Drivers
The database plugins enforce the host security policy on every operation, in both compiled-in and external-plugin form (the policy travels in the plugin handshake):
-
scriptling.sqliteandscriptling.badgerdb(file-backed) — the database path must fall inside--allowed-paths/ the embedder’s allowed paths.":memory:"sqlite databases are always allowed. -
scriptling.sqlandscriptling.valkey(network) — connections dial through the same guard as therequestslibrary, so a network policy applies in full. This answers the common question for MySQL/MariaDB/PostgreSQL DSNs: connecting by hostname or by IP are both covered. A hostname is resolved through the policy’s DNS servers and every resolved address is checked; an IP literal is checked directly — and IP literals are denied by default, somysql://[email protected]/dbneeds the address allowed explicitly:allow_private_ips = truetogether withallow_ip_literals = truefor arbitrary private IP literals, or- the address inside
allow_cidrs(an explicit CIDR grant permits that IP literal withoutallow_ip_literals), or - the hostname inside
allow_hosts(allowlisted hosts are trusted to resolve internally — the recommended way to grant access todb.internal.corp).
Loopback (
postgres://localhoston the same machine) needsallow_loopback = true. The same rules apply tovalkey://URLs.
When no policy is configured the drivers connect without restriction, exactly like the other network libraries.
The same holds for any plugin that advertises the policy capability, and it
is worth being precise about what that means: the policy is delivered to the
plugin and enforced by it. It is not a sandbox around a plugin process, so
--allowed-paths and --network-policy bound what a cooperative plugin
does, never what a malicious one could. Treat third-party plugin binaries
like any other executable you choose to run. For plugins that do cooperate
the protection is real, and the network guard in particular carries genuine
SSRF and DNS-rebinding defences to every dial the plugin makes.
# policy.toml — a script that may reach the database subnet
allow_private_ips = true
allow_hosts = ["db.internal.corp"]Plugin Loading
Registering scriptling.plugin against an unrestricted manager gives scripts load(name, path) — and path is script-supplied, not host-supplied: a filesystem path is resolved and executed directly (no --allowed-paths check), and an http(s):// URL is fetched over a plain transport with no network policy involved. This is a materially bigger capability than either subprocess or an unrestricted requests alone, since it combines arbitrary local execution with arbitrary outbound fetch in one call. “The host chose which plugins to trust” is only true until the first script calls load.
In the CLI this is handled for you when process execution is disabled: --no-subprocess and --disable-lib subprocess also stop a script loading an executable plugin (a subprocess), while plugins the operator supplied with --plugin / --plugin-dir stay loaded and callable, and HTTP(S) plugins — which are not subprocesses — can still be loaded. Embedders get the same effect with plugin.RegisterLibrariesConfigured(p, manager, policy, allowProcesses) (pass false to refuse script-driven executable loads).
Use plugin.WithTransport(plugin.TransportNone) on the scope you register for scripts to close this off while keeping the host’s own pre-loaded plugins fully usable — list, describe, call_function, batch_call, and call_method all keep working; load and unload always fail. This is the pattern that actually makes admin-supplied plugins trusted-and-safe to expose.
If scripts genuinely need to load new plugins at runtime, restrict that to HTTP(S) endpoints over a policy-enforcing transport instead of opening it up entirely: plugin.WithTransport(plugin.TransportHTTP) combined with plugin.WithHTTPTransport routes every load()/call_function for that scope through a transport you build from the same network policy that already governs requests, scriptling.ai, and scriptling.mcp — a script can’t use plugin loading as a side door around restrictions placed on those libraries.
The same idea applies to stdio/executable loading: plugin.WithExecPaths lets scripts load new plugin executables, but only from directories you allow, instead of anywhere the host process can reach. It combines with WithHTTPTransport on the same scope — one restriction governs which executables may be spawned, the other which HTTP(S) endpoints may be dialed, independently of each other and of WithTransport’s all-or-nothing gate on each transport kind.
A scope created with NewScope and no explicit WithTransport/WithExecPaths option inherits both restrictions from its parent scope rather than defaulting to unrestricted — see Nested Scopes. Only an explicit option on the child overrides what it inherited.
Secret Provider Security
Use scriptling.secret when scripts need secrets but should not receive provider URLs, tokens, or other private configuration.
Recommended pattern: Register providers in the host application and expose only aliases to scripts.
import scriptling.secret as secret
db_password = secret.get("prod_vault", "secret/data/app", "password")The script only sees prod_vault, the logical path, and the optional field name. The host owns the real provider configuration.
Resource Limits
Execution Timeout
Always set timeouts for script execution:
import "time"
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := p.EvalWithContext(ctx, code)
if err == context.DeadlineExceeded {
// Script was terminated due to timeout
}Memory and Value Limits
Each interpreter can have a memory limit and a value limit (see Script Resource Limits). Both are off until set, so set them for untrusted code, along with a timeout:
- Memory limit: cancels the interpreter’s running calls when the
estimated memory it holds stays over the limit after a garbage collection.
An interpreter that keeps going over, within a call or between calls,
forces at most one collection every 100 ms, so a script catching
MemoryErrorin a loop cannot stall the process. The cancellation stops the script at its next statement and cannot be caught withtry/exceptor suppressed by awithblock. The same holds for execution timeouts and cancellations, and for aPermissionErrorfrom the filesystem jail or other host restrictions. - Value limit: a single string, bytes value, list, tuple, dict, set,
read body or captured output larger than the limit raises
MemoryError. - Recursion depth is limited to 1000 calls, including calls from Go
through
CallFunction. Source nesting is limited as in Python (200 nested brackets, 99 nested blocks, expressions 20,000 deep), so a deeply nested program, including one run through the sandbox, is a parse error rather than a stack overflow that would end the process.
Memory is estimated, not measured exactly, and every interpreter shares the host’s process. The limits govern memory, not disk: a single extracted archive member or file read is held to the value limit, but a script can still write many files, so bound disk use with operating-system quotas or a size-limited filesystem where it matters. For untrusted code that must not affect anything else, run the interpreters in a separate process with an operating-system memory limit as well.
Code Injection Prevention
Never Execute Untrusted Input Directly
# DANGEROUS: Never do this with scriptling.runtime.sandbox
import scriptling.runtime.sandbox as sandbox
user_input = get_user_code() # e.g., "os.remove('/important/file')"
sandbox.eval(user_input) # Executes arbitrary code!Note: The scriptling.runtime.sandbox library allows executing arbitrary code strings. Never pass untrusted user input directly to it.
Safe Patterns
# SAFE: Use structured data and predefined functions
user_config = get_user_config() # Returns validated dict
name = user_config.get("name", "Anonymous")
greet(name) # Controlled execution
# SAFE: Whitelist allowed operations
allowed_operations = {"add", "subtract", "multiply"}
operation = user_config.get("operation")
if operation in allowed_operations:
result = perform_operation(operation, x, y)Environment Variables
The CLI registers os by default: os.getenv() reads the live process environment and os.environ() returns the snapshot captured when the library was registered. --allowed-paths does not filter either API. For untrusted CLI code, sanitize the host process environment and disable os; embedding hosts can omit the library and expose only selected values:
// A bare scriptling.New() has no libraries; pass only approved values.
p.SetVar("APP_VERSION", "1.0.0")
p.SetVar("API_ENDPOINT", apiEndpoint)Do not rely on omitting sys alone: it is not the CLI’s only environment-access path.
Security Checklist
Use this checklist when deploying Scriptling in production:
- File system access is restricted to specific paths (
os,pathlib,glob) - Network access is disabled or restricted per client (
requests, databases, networking, messaging, AI, MCP) - Execution timeout is configured
-
subprocesslibrary is NOT registered -
sysandosare omitted or carefully controlled for untrusted code - Container, Nomad, plugin, messaging, and provisioning libraries are omitted unless explicitly required
-
scriptling.runtime.sandboxis NOT registered for untrusted code -
scriptling.ai.agentis NOT registered for untrusted code - Environment variables are filtered
- Untrusted user input is validated
- Scripts run with minimal privileges
- Error messages don’t leak sensitive information
- Logs are sanitized before display
- A memory limit, a value limit and a timeout are set for untrusted code
- Runtime KV store size is monitored (unbounded memory)
- Background tasks are properly managed and terminated
Common Attack Vectors
1. Resource Exhaustion
# Consumes all memory
big_list = []
while True:
big_list = big_list + ["x" * 1000000]Mitigation: Set a memory limit and a value limit, and use execution timeouts.
2. Infinite Loops
# Consumes all CPU
while True:
passMitigation: Use execution timeouts.
3. Path Traversal (Protected)
# Attempt to escape allowed directory
import os
os.read_file("../../etc/passwd")Mitigation: Scriptling’s os library validates paths against allowed directories.
4. Information Disclosure
# Read a host secret when the CLI's default os library is enabled
import os
secret = os.getenv("SECRET_TOKEN")Mitigation: Sanitize the process environment and disable or omit os for untrusted code; filesystem path restrictions do not filter environment variables.
Best Practices
- Principle of Least Privilege: Only register the libraries that are absolutely necessary
- Validate All Input: Never trust user-provided code or data
- Use Timeouts: Always set execution time limits
- Restrict File Access: Explicitly whitelist allowed directories for
os,pathlib,glob - Disable Network: Unless needed, keep the sandbox offline (don’t register
requests,scriptling.ai,scriptling.mcp) - Monitor Resource Usage: Watch for unusual memory/CPU consumption
- Sanitize Errors: Don’t expose internal paths or stack traces to users
- Keep Updated: Update Scriptling regularly for security patches
- Limit Runtime State: Monitor
scriptling.runtime.kvstore size and use TTLs - Control Concurrency: Be aware that
scriptling.runtime.syncprimitives can cause deadlocks - Sandbox Isolation: Use
scriptling.runtime.sandboxcarefully - it executes code in isolated environments but still runs in the same process
Reporting Security Issues
If you discover a security vulnerability in Scriptling, please report it responsibly:
- Do NOT create a public issue
- Submit details via our Security Report Form
- Include steps to reproduce
- Allow time for a fix to be released before disclosure
Additional Resources
- Go Integration Basics - Setting up Scriptling in Go
- Sandbox Library - Runtime sandbox configuration
- Library Registration - How to register libraries in Go