Security in Bashkit#
Bashkit is a virtual Bash interpreter designed for safe, sandboxed script execution. Security is a first-class concern, every design decision considers what an untrusted script could do and how to prevent it.
This article gives a high-level overview. For the full threat model with individual threat IDs and mitigation status, see the rustdoc threat model guide.
Core security boundaries#
| Boundary | What it does |
|---|---|
| Virtual filesystem | Scripts run against an in-memory VFS. No real filesystem access by default. Path traversal (../../../etc/passwd) is normalised away. Symlinks are followed inside the VFS only: a target is always a VFS path, so a link can never reach the host. |
| No process execution | exec is excluded entirely. bash -c re-invokes the virtual interpreter instead of spawning a real process. Background jobs (&) parse but run synchronously. |
| Network allowlist | HTTP/HTTPS only, pre-validated against an explicit host allowlist. No DNS resolution, no auto-redirect, no auto-decompression. |
| Resource limits | Configurable caps on commands, loop iterations, recursion depth, AST depth, timeouts, and parser operations prevent denial-of-service from malicious scripts. |
| Filesystem limits | Total bytes, per-file size, file count, path depth, and filename length are all capped to prevent storage exhaustion (zip bombs, tar bombs, recursive copies). |
od -w / od --width accepts at most 65,536 bytes per row. Larger widths
fail before rendering, even with empty input. Numeric fields use at most eight
bytes of stack padding; output capacity is admitted against
ExecutionLimits::max_live_intermediate_bytes before fallible allocation.
This memory budget applies to pipelines and redirected output too.
cd searches CDPATH one candidate at a time. Temporary path and
normalization storage is reserved against max_live_intermediate_bytes
before allocation. Searches also consume the shared work budget and check
cancellation and deadlines, including when every candidate is missing.
Indexed compound-array assignments expand values before evaluating subscripts.
Pending keys, values, fields and container capacity count against
max_live_intermediate_bytes, including repeated writes to the same index.
Exceeding that budget returns a resource-limit error even when the final array
would fit the retained-variable budget.
Command hash entries (hash -p FILE NAME... and automatic PATH lookups) charge
each retained name and pathname against the same live-byte budget before copying.
Entries that survive between executions count toward the next execution’s budget.
Deleting, replacing, evicting or clearing entries releases their storage; subshells
share pathname storage while maintaining independent hash state. The table also
retains its 512-entry ceiling.
MemoryLimits::max_function_body_bytes also includes retained function-definition
filenames and metadata keys. Filename storage is shared; each function is charged
conservatively before insertion. An oversized source operand therefore fails
with a resource-limit error even when redundant slashes normalize to a short VFS
path. Redefinition replaces the charge, unset releases it, and subshell rollback
restores filenames together with functions. Executable child scripts discard
their own metadata on return. Functions and their admitted metadata
persist across executions; shell-state restore discards previous filenames because
the serialized state carries function source without definition filenames.
Recursive grep -R skips directory symlinks that resolve to an ancestor and
reports warning: recursive directory loop (suppressed by -s). Separate
aliases of a non-ancestor directory remain searchable. Traversal uses the shared
execution work and deadline budget; file contents are admitted against aggregate
input and live intermediate limits and scanned one file at a time. -q stops
at the first match, before further traversal.
Tilde expansion (~, ~+, ~-, including repeated assignment prefixes after
:) checks ExecutionLimits::max_live_intermediate_bytes before growing its
output and charges copied bytes to max_work_units. Large directory values
cannot bypass these limits by expanding before variable insertion. A refused
expansion returns a resource-limit error; the next host execution receives a
fresh execution budget.
Syntax-error reports are limited to 1 KiB during construction, including an
explicit [diagnostic truncated] marker when text is omitted. This prevents
multiline unexpected tokens and long bash -c shell names from multiplying
host-memory usage before stderr capture. Ordinary short reports retain Bash’s
text, line numbers, and exit status.
Threat model#
Prompt expansion (${x@P} and interactive prompts) shares the execution work,
aggregate-input, cancellation and deadline budgets. Recursive prompt reparses
stop at min(max_ast_depth, 8) with a resource-limit error, including cycles
through command substitutions. Raising the AST limit cannot remove this host
stack protection. Finite prompt expansion and shopt -u promptvars retain their
normal behavior within these limits.
Bashkit maintains a living threat model in knowledge/security/threat-model.md
with stable threat IDs across these categories:
| Category | ID prefix | Examples |
|---|---|---|
| Denial of Service | TM-DOS | Resource exhaustion, infinite loops, parser bombs |
| Sandbox Escape | TM-ESC | Path traversal, real FS access, privilege escalation |
| Information Disclosure | TM-INF | Secret leakage, host info exposure, data exfiltration |
| Injection | TM-INJ | Command injection, variable namespace pollution |
| Network | TM-NET | DNS rebinding, allowlist bypass, response flooding |
| Multi-Tenant Isolation | TM-ISO | Cross-tenant data leaks |
| Internal Errors | TM-INT | Panics, error message information leaks |
| Git | TM-GIT | Repo access control, remote URL injection |
| Logging | TM-LOG | Sensitive data in logs, log injection |
| Python Sandbox | TM-PY | Monty resource limits, VFS bridge escapes |
| Unicode | TM-UNI | Byte-boundary panics, homoglyph attacks |
The full threat model, including mitigation status for each threat, is published in the rustdoc: bashkit::threat_model.
POSIX deviations for security#
Bashkit intentionally deviates from POSIX where compliance would compromise the sandbox. Key exclusions:
exec: would break sandbox containment (TM-ESC-005)trap: conflicts with the stateless execution model- Real process spawning: all subprocess commands stay within the virtual interpreter (
TM-ESC-015)
These decisions are documented in knowledge/operations/limitations.md.
Security testing#
Bashkit uses multiple layers of security testing:
Threat model tests: 232 tests in threat_model_tests.rs that directly
validate mitigations against documented threat IDs. Each test maps to a specific
TM-* threat.
Fail-point injection: A framework defined in knowledge/security/security-testing.md
that injects failures at specific points to verify the interpreter handles them
safely. 14+ tests in security_failpoint_tests.rs.
Network security tests: 68 tests covering allowlist enforcement, URL validation, timeout behaviour, and response limits.
Error handling tests: 39 tests verifying that builtins wrapped with
catch_unwind never leak panic messages, stack traces, or memory addresses.
Logging security tests: 26 tests confirming that sensitive data (passwords, tokens, API keys, JWTs) is redacted in logs and that log injection is prevented.
Fuzz testing: Parser and lexer fuzzing to catch panics and unexpected behaviour on malformed input.
Differential tests: Compare Bashkit output against real Bash to ensure behaviour parity where expected, and confirm intentional divergences where security requires it.
Arithmetic limits cover textual dollar expansion inside expressions.
Nested array subscripts share the enclosing expression’s recursion and work
limits, including quoted arguments to let. Exceeding either limit returns
an arithmetic error; subsequent executions can reuse the session.
Panic safety#
All builtin commands are wrapped with catch_unwind. If a builtin panics, the
error is caught and converted to a sanitised error message, no stack traces, no
memory addresses, no real filesystem paths leak to the caller (TM-INT-001,
TM-INT-002).
Reporting security issues#
Do not open a public GitHub issue for security vulnerabilities.
Email: security@everruns.com
Please include a description of the vulnerability, steps to reproduce, and potential impact. We acknowledge reports within 48 hours, provide an initial assessment within 7 days, and target 30-day resolution for critical issues.
See SECURITY.md for the full policy.
We appreciate responsible disclosure and acknowledge researchers who report valid vulnerabilities (with permission).