Qore provides comprehensive sandboxing capabilities for safely executing untrusted or semi-trusted code. This is essential for applications that need to run user-provided code, such as:
- Multi-tenant SaaS platforms
- Plugin and extension systems
- Online code execution environments
- Web application scripting engines
- Workflow and automation systems
Qore offers two complementary approaches to sandboxing:
- **Parse Options** - Binary on/off restrictions that completely disable categories of functionality
- **SandboxManager** - Fine-grained, configurable restrictions with allow/deny lists and resource limits
Parse Option Restrictions
Parse options provide a simple way to completely disable access to certain system capabilities. These are enforced at parse time, meaning code that attempts to use restricted features will fail to parse rather than failing at runtime.
Overview of Security Parse Options
The following parse options restrict system access:
| Parse Option | Description | |--------------|-------------| | PO_NO_FILESYSTEM | Disables all filesystem access | | PO_NO_NETWORK | Disables all network access | | PO_NO_THREAD_CONTROL | Disables thread creation and control | | PO_NO_THREAD_CLASSES | Disables thread synchronization classes | | PO_NO_THREADS | Combines PO_NO_THREAD_CONTROL and PO_NO_THREAD_CLASSES | | PO_NO_PROCESS_CONTROL | Disables process control (exit, fork, etc.) | | PO_NO_EXTERNAL_PROCESS | Disables spawning external processes | | PO_NO_EXTERNAL_INFO | Disables access to external system information | | PO_NO_EXTERNAL_ACCESS | Combines multiple restrictions for external access | | PO_NO_DATABASE | Disables database access | | PO_NO_MODULES | Disables loading modules | | PO_NO_IO | Disables all I/O operations | | PO_NO_GUI | Disables GUI functionality | | PO_NO_UNCONTROLLED_APIS | Disables APIs that could bypass sandboxing |
Using Parse Options
Parse options can be set when creating a Program object:
%new-style
%require-types
%strict-args
Program p(PO_NEW_STYLE | PO_NO_FILESYSTEM | PO_NO_NETWORK);
try {
p.parse(user_code, "user_script");
} catch (hash<ExceptionInfo> ex) {
printf("Parse error: %s\n", ex.desc);
}
Parse options can also be set via parse directives in the code itself:
%no-filesystem
%no-network
Or via command-line options:
qore --no-filesystem --no-network script.q
Lockdown Parse Option
The PO_LOCKDOWN parse option combines many restrictions for maximum security:
Parse Option Limitations
While parse options are effective for completely disabling functionality, they have limitations:
- **Binary**: Cannot allow partial access (e.g., allow some files but not others)
- **No resource limits**: Cannot limit memory, CPU time, or thread count
- **No interruption**: Cannot stop runaway code (infinite loops)
- **No SSRF prevention**: Cannot block specific IP ranges
For these advanced use cases, use the SandboxManager class.
SandboxManager Class
The SandboxManager class provides fine-grained, configurable sandboxing with support for:
Basic Usage
%new-style
%require-types
%strict-args
SandboxManager sm();
sm.setMemoryLimit(100 * 1024 * 1024);
sm.setWallTimeLimit(30000);
sm.setMaxThreads(4);
sm.blockPrivateNetworks();
sm.setFilesystemDefaultPolicy(False);
Program p(PO_NEW_STYLE | PO_STRICT_ARGS);
p.setSandboxManager(sm);
p.parse(user_code, "user_script");
auto result = p.callFunction("main");
Filesystem Security
The filesystem security system controls which paths can be accessed by sandboxed code. It supports:
- **Sandbox root**: Restrict all access to within a specific directory (chroot-like)
- **Allow/deny lists**: Fine-grained path-based access control
- **Access modes**: Control read, write, execute, delete, and create permissions separately
Parse Options vs SandboxManager
| Feature | PO_NO_FILESYSTEM | SandboxManager | |---------|------------------|----------------| | Block all filesystem access | Yes | Yes (default deny policy) | | Allow specific directories | No | Yes | | Deny specific directories | No | Yes | | Different read/write permissions | No | Yes | | Sandbox root (chroot-like) | No | Yes |
Sandbox Root
Setting a sandbox root restricts all filesystem access to within that directory:
SandboxManager sm();
sm.setFilesystemSandboxRoot("/home/sandbox/userdata");
The sandbox root:
- Automatically resolves symlinks to prevent escapes
- Blocks path traversal attacks (../)
- Uses realpath() for canonical path resolution
Allow and Deny Lists
For more granular control, use allow and deny lists:
SandboxManager sm();
sm.addFilesystemAllowedPath("/data/public", QSEC_READ);
sm.addFilesystemAllowedPath("/data/uploads", QSEC_READ | QSEC_WRITE | QSEC_CREATE);
sm.addFilesystemDeniedPath("/data/uploads/secrets");
- Note
- Denied paths always take precedence over allowed paths.
Access Mode Constants
The following constants control filesystem access modes:
| Constant | Value | Description | |----------|-------|-------------| | QSEC_READ | 1 | Read access to files | | QSEC_WRITE | 2 | Write/modify access to files | | QSEC_EXECUTE | 4 | Execute access to files | | QSEC_DELETE | 8 | Delete files | | QSEC_CREATE | 16 | Create new files | | QSEC_ALL | 31 | All access modes combined |
Default Policy
By default, unlisted paths are denied. You can change this behavior:
sm.setFilesystemDefaultPolicy(False);
sm.setFilesystemDefaultPolicy(True);
Network Security
The network security system controls socket connections and provides SSRF (Server-Side Request Forgery) prevention.
Parse Options vs SandboxManager
| Feature | PO_NO_NETWORK | SandboxManager | |---------|---------------|----------------| | Block all network access | Yes | Yes (default deny policy) | | Allow specific hosts | No | Yes | | Allow specific IP ranges | No | Yes | | Block private networks (SSRF) | No | Yes | | Port restrictions | No | Yes | | Protocol filtering (TCP/UDP/UNIX) | No | Yes |
SSRF Prevention
Server-Side Request Forgery (SSRF) attacks trick applications into making requests to internal resources. The blockPrivateNetworks() method blocks all private and internal network ranges:
SandboxManager sm();
sm.blockPrivateNetworks();
sm.setNetworkDefaultPolicy(True);
- Note
- Security checks occur AFTER DNS resolution to prevent attacks using hostnames that resolve to internal IP addresses.
Host and IP Restrictions
Control access by hostname patterns and IP ranges:
SandboxManager sm();
sm.addNetworkAllowedHost("api.example.com");
sm.addNetworkAllowedHost("*.trusted-domain.org");
sm.addNetworkAllowedIPRange("203.0.113.0/24");
sm.addNetworkAllowedIPRange("2001:db8::/32");
sm.addNetworkDeniedIPRange("192.168.0.0/16");
Port Restrictions
Control which ports can be connected to:
SandboxManager sm();
sm.allowHTTPSOnly();
sm.addNetworkAllowedPort(443, QSEC_NET_TCP);
sm.addNetworkAllowedPort(80, QSEC_NET_TCP);
sm.addNetworkAllowedPort(53, QSEC_NET_ALL);
sm.addNetworkAllowedPortRange(8000, 9000, QSEC_NET_TCP);
Protocol Constants
The following constants control network protocols:
| Constant | Value | Description | |----------|-------|-------------| | QSEC_NET_TCP | 1 | TCP protocol | | QSEC_NET_UDP | 2 | UDP protocol | | QSEC_NET_UNIX | 4 | UNIX domain sockets | | QSEC_NET_ALL | 7 | All protocols |
Resource Limits
Resource limits prevent denial-of-service attacks and runaway code. Unlike parse options, SandboxManager can limit resources rather than completely disable features.
SandboxManager sm();
sm.setMemoryLimit(100 * 1024 * 1024);
sm.setCPUTimeLimit(10000);
sm.setWallTimeLimit(30000);
sm.setMaxThreads(4);
sm.setMaxRecursionDepth(100);
When limits are exceeded, appropriate exceptions are raised:
| Exception | Cause | |-----------|-------| | SANDBOX-MEMORY-LIMIT | Memory limit exceeded | | SANDBOX-TIMEOUT | CPU or wall time limit exceeded | | SANDBOX-THREAD-LIMIT | Thread limit exceeded | | SANDBOX-RECURSION-LIMIT | Recursion depth exceeded |
Safe Code Interruption
The interrupt mechanism allows safely stopping sandboxed code that is stuck in loops or blocking operations. This is unique to SandboxManager - parse options cannot stop running code.
SandboxManager sm();
Program p(PO_NEW_STYLE);
p.setSandboxManager(sm);
p.parse(untrusted_code, "user_script");
Counter done(1);
background sub() {
try {
p.callFunction("main");
} catch (hash<ExceptionInfo> ex) {
}
done.dec();
}();
usleep(5s);
if (done.getCount() > 0) {
sm.requestInterrupt();
}
done.waitForZero();
sm.clearInterrupt();
Interrupt Check Points
The interrupt mechanism is checked at:
- **Loop boundaries**: while, for, foreach, do-while (checked before each iteration)
- **Sleep functions**: sleep(), usleep() (polled every 100ms)
- **Lock acquisition**: Mutex::lock(), RWLock::readLock(), RWLock::writeLock() (polled every 500ms)
- **Auto-lock constructors**: AutoLock, AutoReadLock, AutoWriteLock (polled every 500ms)
- **Queue operations**: Queue::get(), Queue::pop() (polled every 500ms)
- **Thread synchronization**: Counter::waitForZero(), Condition::wait(), Gate::enter() (polled every 500ms)
- **File I/O**: File read, write, and lock operations (polled at I/O poll interval)
- **Stream I/O**: PipeInputStream::read(), PipeOutputStream::write() (polled at I/O poll interval)
- **Standard I/O**: StdoutOutputStream::write(), StderrOutputStream::write() (polled at I/O poll interval)
- **Database operations**: Datasource transaction lock acquisition (polled at I/O poll interval)
When interrupted, a PROGRAM-INTERRUPTED exception is raised. For external process functions (system(), backquote(), and the backquote operator), a SandboxManager also starts the child in a new process group so that requestInterrupt() can terminate the entire process group. Without a SandboxManager, children remain in the current process group and receive terminal signals (for example, Ctrl-C) along with the parent.
Polling Mechanism
For blocking operations, the interrupt mechanism uses a polling approach where the operation is performed with a timeout, then the interrupt status is checked, and the operation is retried if no interrupt was requested. This ensures that blocked threads can be interrupted even when waiting on locks or I/O operations.
| Operation Type | Polling Interval | Latency | |----------------|------------------|---------| | sleep(), usleep() | 100ms | Fast response for timed waits | | Threading primitives | 500ms | Lower overhead for lock contention | | I/O operations | 500ms | Consistent with threading primitives |
- Note
- The polling mechanism only incurs overhead when a SandboxManager is attached to the Program. Normal programs without a SandboxManager have zero polling overhead.
Security Presets
Two convenience presets are available for common use cases:
Lockdown Mode
Maximum security for completely untrusted code:
SandboxManager sm = SandboxManager::createLockdown();
Web-Safe Mode
Suitable for web application sandboxing where some network access is needed:
SandboxManager sm = SandboxManager::createWebSafe();
Combining Parse Options and SandboxManager
Parse options and SandboxManager can be used together for defense in depth:
Program p(PO_NEW_STYLE | PO_NO_EXTERNAL_PROCESS | PO_NO_DATABASE);
SandboxManager sm();
sm.addFilesystemAllowedPath("/data/uploads", QSEC_READ | QSEC_WRITE);
sm.blockPrivateNetworks();
sm.setNetworkDefaultPolicy(True);
sm.setMemoryLimit(50 * 1024 * 1024);
sm.setWallTimeLimit(10000);
p.setSandboxManager(sm);
Configuration Inspection
You can inspect the current sandbox configuration:
SandboxManager sm();
sm.blockPrivateNetworks();
sm.setMemoryLimit(100 * 1024 * 1024);
hash<auto> config = sm.getConfiguration();
printf("Config: %N\n", config);
hash<auto> fs_config = sm.getFilesystemConfiguration();
hash<auto> net_config = sm.getNetworkConfiguration();
Security Exceptions
The following exceptions may be raised by the sandbox system:
| Exception | Source | Description | |-----------|--------|-------------| | FILESYSTEM-ACCESS-DENIED | SandboxManager | Filesystem access blocked by security policy | | NETWORK-ACCESS-DENIED | SandboxManager | Network connection blocked by security policy | | PROGRAM-INTERRUPTED | SandboxManager | Execution interrupted via requestInterrupt() | | SANDBOX-MEMORY-LIMIT | SandboxManager | Memory limit exceeded | | SANDBOX-TIMEOUT | SandboxManager | CPU or wall time limit exceeded | | SANDBOX-THREAD-LIMIT | SandboxManager | Thread limit exceeded | | SANDBOX-RECURSION-LIMIT | SandboxManager | Recursion depth exceeded | | Parse error | Parse Options | Code uses disabled functionality |
Best Practices
- **Defense in depth**: Combine parse options with SandboxManager
- Use parse options to completely disable unneeded features
- Use SandboxManager for fine-grained control of allowed features
- **Always use blockPrivateNetworks()** for web-facing applications
- Essential for SSRF prevention
- Blocks access to cloud metadata endpoints (169.254.169.254)
- Blocks access to internal services
- **Set reasonable resource limits**
- Memory limits prevent memory exhaustion
- Time limits prevent infinite loops
- Thread limits prevent fork bombs
- **Use deny-by-default policies**
- Only explicitly allow what is needed
- Denied paths/IPs take precedence over allowed
- **Implement interrupt handling** for long-running operations
- Use background threads with timeout monitoring
- Call requestInterrupt() if execution exceeds expected time
- **Validate paths before adding to allow lists**
- Use absolute canonical paths
- Be aware of symlink escapes
- Test with edge cases
- **Use lockdown mode for pure computation**
- When code doesn't need external access, use createLockdown()
- Provides maximum security with minimal configuration
Security Checklist
Before deploying sandboxed code execution:
- [ ] Define maximum resource limits (memory, CPU, wall time, threads)
- [ ] Block private networks if any network access is allowed
- [ ] Use deny-by-default for filesystem access
- [ ] Implement timeout monitoring with interrupt handling
- [ ] Disable unneeded features with parse options
- [ ] Test with malicious input (infinite loops, memory bombs, SSRF attempts)
- [ ] Log security exceptions for monitoring
- See also
- SandboxManager
-
Program
-
PO_LOCKDOWN
-
PO_NO_EXTERNAL_ACCESS
- Since
- Qore 2.3 (SandboxManager class)