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.
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.
Threat model#
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.
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).