Skip to main content

Sandboxing

WASP executes code, runs browsers, and modifies files. This page documents every sandbox layer and what each one does — and does not — guarantee.

Container isolation​

All WASP services run in Docker containers with non-root users (UID 1000), except agent-broker which needs root for Docker socket access.

The agent container itself:

  • Non-root user (UID 1000)
  • No --privileged flag
  • Docker CLI talks to agent-broker, not the socket directly
  • All destructive Docker operations go through the broker's allowlist

Shell sandbox​

The shell skill (skills/builtin/shell.py, RESTRICTED) executes commands via asyncio.create_subprocess_exec:

process = await asyncio.create_subprocess_exec(
*shlex.split(command),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd="/data",
)

Constraints:

SettingValue
Default timeout60 s
Max timeout120 s
Output cap8 000 chars
Working dir/data
Useragent (UID 1000)

Every invocation:

  1. Passes the command through _redact_command() — strips sk-*, AIza*, xai-*, hf_*, and key=value / password=value patterns.
  2. Writes one AuditLog row with action="skill.shell", redacted command, exit code, and goal_id (when invoked from a goal).

The shell skill has full access to the agent container's filesystem and can run any command the agent user can run. The container itself is the sandbox. This is defense-in-depth, not a hard sandbox.

Python sandbox​

The python_exec skill writes code to a temp file and executes as a subprocess:

with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f:
f.write(code)
tmpfile = f.name

process = await asyncio.create_subprocess_exec(
sys.executable, tmpfile,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)

Constraints: same timeout/output caps as shell. Code is parsed with ast.parse and the import statements + dangerous-call sites are checked against _DANGEROUS_IMPORTS:

subprocess, os, sys, pty, ctypes, pickle,
marshal, importlib, __import__, eval, exec, compile

The AST scanner blocks code that uses these names directly. A sophisticated prompt injection could potentially bypass this through indirect means; the AST check is defense-in-depth, not a complete sandbox.

Skill Evolution sandbox​

When the Skill Evolution job synthesizes a composite skill, the generated code passes the same AST validation as python_exec. Operator approval at /skill-evolution is required before activation.

SSRF protection​

SSRF protection is centralized in src/utils/network_safety.py (v2.7). Every skill that makes outbound HTTP requests imports validate_url_for_request() from this module:

  • http_request
  • fetch_url
  • scrape
  • browser (URL blocklist at navigation time)
  • monitors
  • subscriptions (RSS + price feeds)

Blocked targets:

RFC-1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Loopback: 127.0.0.0/8, ::1
Link-local: 169.254.0.0/16, fe80::/10
Cloud metadata: 169.254.169.254, fd00:ec2::254,
metadata.google.internal, metadata.azure.com
project-blocked: api.telegram.org (avoid leaking the operator's bot token)

DNS rebinding protection​

Hostname resolution is performed once, and the resolved IP is sent to the HTTP client (via httpx's transport config), so an attacker can't return a public IP to pass the check and then resolve to an internal IP on the actual request.

Manual redirect re-validation​

follow_redirects=True is disabled. Skills follow redirects manually (max 5 hops), re-validating each Location: header against the SSRF guard. This blocks the "redirect to localhost after a public-looking first URL" attack.

19 unit tests cover the SSRF guard (tests/test_ssrf_guard.py).

browser URL blocklist​

The browser skill blocks URLs at navigation time:

PatternReason
file://Local file access
javascript:XSS vector
data:Embedded data exfiltration
vbscript:Legacy IE script
RFC-1918 ranges, 127.0.0.0/8Private network
169.254.169.254, fd00:ec2::254Cloud metadata IMDS

Multi-URL Aggregator: Error: prefix detection​

When auto-detect resolves multiple URLs in one user message, the multi-URL aggregator builds a per-URL outcome list. The browser skill returns success=True even when its output begins with Error: URL blocked... (success only means the skill itself didn't crash).

The aggregator detects the Error: prefix BEFORE checking [CAPTURE_VALID] markers — so SSRF-blocked URLs are correctly labeled ❌ with the first-line error, not ✅ navigated.

IconMeaning
✅ navigatedSuccessfully reached the URL without screenshot
✅ screenshot sentCapture taken and attached
🚫 blockedLogin wall or captcha ([CAPTURE_VALID: false])
❌ <error line>Error: prefix detected (SSRF, file://, RFC-1918, etc.)

File system access​

The file_ops skill is constrained to /data/. The self_improve skill uses os.path.realpath() for containment checks:

real_path = os.path.realpath(requested_path)
if not real_path.startswith("/app/src/"):
raise PermissionError("Path traversal attempt blocked")

Symlinks are resolved before the containment check, so /etc/passwd-via-symlink attacks fail.

Self-Improve safety​

Before writing any .py file via POST /api/{proposal_id}/apply:

  1. ast.parse(content) validates Python syntax. SyntaxError returns HTTP 400 with the error truncated to 120 chars; no file is written.
  2. shutil.copy2() creates a timestamped backup at /data/src_patches/backup_{ts}_{filename} before overwrite.
  3. The success JSON returns backup_path so the operator can see the rollback target.

Self-Improve Soft Safety Gate​

_self_improve_soft_gate() runs before any write/patch/apply_patch action. Three-tier decision:

TierCondition
BLOCKCritical path (sandbox.py, control_layer.py, behavioral.py, response_grounder.py, etc.) AND weakening pattern AND (is_large_patch OR is_dense_patch)
WARNCritical path AND (is_large_patch OR is_dense_patch) without weakening
ALLOWOtherwise

Diff signals: patch_length > 2000 → is_large_patch; avg_line_length > 120 → is_dense_patch.

Weakening patterns (13 of them) include: disable sandbox, bypass guard, skip confirmation, _HIGH_RISK_ACTIONS=frozenset(), etc.

All decisions log via self_improve.soft_gate_analysis with full metrics.

SHA-256 sidecar integrity​

Every persisted patch in /data/src_patches/ gets a .sha256 sidecar. apply_persisted_patches() at startup:

  • Skips .sha256 files themselves
  • Verifies sidecar if present (mismatch → log warning + skip)
  • Applies legacy patches without sidecars with an info log
  • Catches all checksum errors fail-open — startup never crashes

Browser isolation​

The browser skill runs Chromium in headless mode:

  • No GPU acceleration (headless)
  • Profile directories isolated per session name (/data/browser_sessions/<name>/)
  • Screenshots saved to /data/screenshots/
  • Cookies persist across calls when using a named session
  • Idle Reaper daemon closes Chromium sessions idle > 300 s

Anticipatory simulation​

For RESTRICTED and PRIVILEGED skills, the executor runs a pre-execution simulation. The LLM predicts the outcome and any risks; the result is appended to the skill output for the next round of self-reflection. This is not a security control — it's a cognitive self-check that allows the LLM to reconsider an action before execution.

Cold-Start Hallucination Guard​

A short message arriving on a fresh chat with no prior context is one of the most reliable hallucination triggers. The _is_low_intent() guard returns True for:

  1. Single ambiguous token — short confirmations and acknowledgements (multilingual frozenset).
  2. Emoji / digit / punctuation-only message.
  3. Context-required phrase without anchor — phrases that explicitly refer to prior interaction (e.g., "do the same", "again", "same as before", and equivalents).
  4. ≤ 2 tokens AND every token is in the ambiguous set.

When low-intent + no scheduled-language match + no last_exchange anchor, the handler returns a clarification fast-path in the user's detected language and never invokes the LLM.

This is a deterministic guard, not a model behavior — zero token cost, zero hallucination risk on ambiguous cold-start input.

Secrets handling​

Credentials are stored in Redis encrypted via SecretVault. The vault key is derived from DASHBOARD_SECRET. Never stored in plaintext: integration API keys, Gmail credentials, OAuth tokens.

All audit log entries pass through redact() (utils/redaction.py) which strips OpenAI sk-, Anthropic sk-ant-, Google AIza, xAI xai-, HuggingFace hf_, AWS AKIA, Stripe sk_live_, Slack xox*, SendGrid SG., Bearer tokens, and key=value password patterns.

Security best practices​

  1. Keep TELEGRAM_ALLOWED_USERS restricted to your Telegram user IDs.
  2. Use a strong DASHBOARD_SECRET (≥ 32 random chars).
  3. Use a strong MEDIA_SIGNING_SECRET (≥ 64 random hex chars).
  4. Don't expose port 8080 directly — use the nginx proxy.
  5. Regularly review audit logs at /audit.
  6. Consider SKILL_EVOLUTION_ENABLED=false for maximum code safety.
  7. Run on a dedicated server — don't share with other services.

See also​