Body
Severity
Critical — CVSS v3.1 Base Score: 9.6 — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Note: The prompt-injection variant (Attack Surface 3) drops PR:L, raising the effective score toward 10.0, since attacker-controlled content processed by the agent reaches this sink without human interaction.
This is not a duplicate of the closed #30. #30 reported that execute_backend_code() had no sandbox. The fix added an AST import/builtin allowlist (get_code_warnings), a builtins-scrubbing subprocess preamble, and a minimal environment (p_minimal_env). This report demonstrates that the mitigation is fully bypassable and, on the /api/outputs/execute path, actually executes bypassing code in the more-privileged force-mode environment.
CWE Classification
| CWE ID |
Name |
| CWE-265 |
Improper Privilege Management / Sandbox Escape |
| CWE-693 |
Protection Mechanism Failure |
| CWE-94 |
Improper Control of Generation of Code ("Code Injection") |
| CWE-78 |
OS Command Injection |
Affected Components
| File |
Function |
Role |
backend/apps/outputs/executor.py |
execute_backend_code() preamble (L181–198) |
Scrubs builtins but leaves sys / sys.modules intact |
backend/apps/outputs/executor.py |
get_code_warnings() (L34–76) |
AST gate sees no Import node and no blocked builtin name, returning [] |
backend/apps/outputs/executor.py |
p_minimal_env() (L109–146) |
force=True inherits full environment including PATH / COMSPEC |
backend/apps/outputs/outputs.py |
execute_output() (/api/outputs/execute, L699–736) |
Empty warnings skip HITL and invoke executor with skip_validation=True |
Vulnerability Description
The Output "backend code" sandbox is enforced by two host-side controls:
-
AST allowlist (get_code_warnings)
- Rejects imports outside
P_ALLOWED_MODULES
- Rejects calls to blocked builtins (
eval, open, __import__, ...)
-
Subprocess preamble
- Removes dangerous names from
builtins inside the child process
The preamble begins with:
import json, sys, io, builtins
and then only scrubs builtins.
It never removes sys, and never touches sys.modules.
Because os has already been imported into the interpreter (transitively via json), user code can retrieve the live module without using any import statement and without referencing any blocked builtin:
os_mod = sys.modules["os"]
get_code_warnings() walks the AST and finds:
- no
ast.Import
- no
ast.ImportFrom
- no blocked builtin call
Therefore it returns:
Every protection layer is bypassed.
Strict mode (skip_validation=False)
The following APIs become reachable:
os.open
os.read
os.write
os.remove
os.listdir
os.environ
allowing:
- arbitrary file reads
- arbitrary file overwrite
- arbitrary deletion
- environment disclosure
(os.system() only fails because the minimal environment strips PATH and COMSPEC; filesystem syscalls require neither.)
/api/outputs/execute
Empty warnings flow directly into:
execute_backend_code(..., skip_validation=True)
which invokes:
p_minimal_env(force=True)
The child inherits the full host environment, including PATH and COMSPEC.
Consequently:
successfully invokes the shell, yielding arbitrary command execution with:
- no approval dialog
- no user consent
- no "Run Anyway" gate
because the AST warning list remained empty.
Root Cause Analysis
1. Incomplete preamble scrub
The child removes dangerous builtins but leaves:
fully accessible.
Any already-imported dangerous module (os, nt, io, etc.) is one dictionary lookup away.
The AST allowlist cannot detect this because no import statement executes.
2. Empty AST warnings are treated as trusted
execute_output() interprets:
get_code_warnings(...) == []
as authorization to invoke:
which selects the force-mode environment.
As a result, the least-scrutinized code executes in the most-privileged environment, the opposite of the intended security model.
Attack Scenario
Surface 1 — Local process with install token
Any local process reads:
from:
%APPDATA%/OpenSwarm/data/
~/Library/Application Support/OpenSwarm/data/
~/.local/share/OpenSwarm/data/
and submits a crafted Output to:
Surface 2 — Persist then execute
Create an Output whose backend.py uses the sys.modules technique and execute it.
Surface 3 — Indirect prompt injection
Poisoned content retrieved through MCP tools (Gmail, Drive, Web, etc.) instructs the LLM to generate an Output containing the bypass payload.
Since the AST gate remains silent, no approval dialog is shown.
Proof of Concept
All PoCs were executed against the current backend/apps/outputs/executor.py.
Marker files were created inside the OS temporary directory and removed afterward.
No data left the machine.
PoC A — Arbitrary file read (skip_validation=False)
code = '''
os_mod = sys.modules['os']
fd = os_mod.open(TARGET_ABS_PATH, os_mod.O_RDONLY)
result = {'read': os_mod.read(fd, 90).decode('utf-8','replace')}
os_mod.close(fd)
'''
get_code_warnings(code) -> []
Result:
{'read': '"""Per-install bearer token gating the localhost API...'}
PoC B — Arbitrary file overwrite
code = '''
os_mod = sys.modules['os']
fd = os_mod.open(VICTIM_ABS_PATH, os_mod.O_WRONLY | os_mod.O_TRUNC)
os_mod.write(fd, b'OVERWRITTEN-BY-SANDBOXED-CODE')
os_mod.close(fd)
result = {'wrote': True}
'''
Victim file afterwards:
OVERWRITTEN-BY-SANDBOXED-CODE
PoC C — Command execution on /execute
code = '''
os_mod = sys.modules['os']
result = {'rc': os_mod.system('echo pwned-via-execute-endpoint > MARKER')}
'''
get_code_warnings(code) -> []
Result:
Marker file:
pwned-via-execute-endpoint
Impact
Steps To Reproduce
- Start OpenSwarm.
- Obtain the per-install token from:
- Create an Output whose
backend.py contains PoC C.
- Execute:
POST /api/outputs/execute
- Observe:
backend_result.rc == 0
- Marker file exists
- No approval prompt appears
Technical Evidence
executor.py (L181–198)
preamble = (
"import json, sys, io, builtins\n"
"for _b in ('exec','eval','compile','open','input',\n"
" 'breakpoint','exit','quit'):\n"
" try: delattr(builtins, _b)\n"
" except AttributeError: pass\n"
...
)
sys and sys.modules remain fully accessible.
outputs.py (L722–730)
if not body.force:
warnings_out = get_code_warnings(output.backend_code)
...
if not warnings_out:
exec_result = await execute_backend_code(
output.backend_code,
body.input_data,
skip_validation=True
)
Empty warnings invoke the executor with the force-mode environment.
Suggested Remediation
P0 — Remove module access in the child preamble
preamble = (
"import json, sys, io, builtins\n"
"_dump = json.dump\n"
"for _b in ('exec','eval','compile','open','input','breakpoint','exit','quit'):\n"
" try: delattr(builtins, _b)\n"
" except AttributeError: pass\n"
"for _m in ('os','nt','posix','sys','subprocess','socket','ctypes','importlib'):\n"
" sys.modules.pop(_m, None)\n"
...
)
Capture any required io, json, or sys.stdout references before removing sys.
Add a self-test verifying:
raises inside the child.
P0 — Decouple AST warnings from force-mode execution
execute_output() should not invoke:
merely because the warning list is empty.
Only explicit user approval (force=True) should enable the force-mode environment.
P1 — Replace AST filtering with real isolation
Treat AST validation as a usability feature rather than a security boundary.
Execute backend code inside OS-enforced isolation:
- Linux namespaces + seccomp
- Read-only container
--network=none
- Windows AppContainer / Windows Sandbox
Drop PATH and jail the working directory.
References
Body
Severity
Critical — CVSS v3.1 Base Score: 9.6 — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
This is not a duplicate of the closed #30. #30 reported that
execute_backend_code()had no sandbox. The fix added an AST import/builtin allowlist (get_code_warnings), a builtins-scrubbing subprocess preamble, and a minimal environment (p_minimal_env). This report demonstrates that the mitigation is fully bypassable and, on the/api/outputs/executepath, actually executes bypassing code in the more-privileged force-mode environment.CWE Classification
Affected Components
backend/apps/outputs/executor.pyexecute_backend_code()preamble (L181–198)sys/sys.modulesintactbackend/apps/outputs/executor.pyget_code_warnings()(L34–76)Importnode and no blocked builtin name, returning[]backend/apps/outputs/executor.pyp_minimal_env()(L109–146)force=Trueinherits full environment includingPATH/COMSPECbackend/apps/outputs/outputs.pyexecute_output()(/api/outputs/execute, L699–736)skip_validation=TrueVulnerability Description
The Output "backend code" sandbox is enforced by two host-side controls:
AST allowlist (
get_code_warnings)P_ALLOWED_MODULESeval,open,__import__, ...)Subprocess preamble
builtinsinside the child processThe preamble begins with:
and then only scrubs
builtins.It never removes
sys, and never touchessys.modules.Because
oshas already been imported into the interpreter (transitively viajson), user code can retrieve the live module without using any import statement and without referencing any blocked builtin:get_code_warnings()walks the AST and finds:ast.Importast.ImportFromTherefore it returns:
Every protection layer is bypassed.
Strict mode (
skip_validation=False)The following APIs become reachable:
os.openos.reados.writeos.removeos.listdiros.environallowing:
(
os.system()only fails because the minimal environment stripsPATHandCOMSPEC; filesystem syscalls require neither.)/api/outputs/executeEmpty warnings flow directly into:
which invokes:
The child inherits the full host environment, including
PATHandCOMSPEC.Consequently:
successfully invokes the shell, yielding arbitrary command execution with:
because the AST warning list remained empty.
Root Cause Analysis
1. Incomplete preamble scrub
The child removes dangerous builtins but leaves:
syssys.modulesfully accessible.
Any already-imported dangerous module (
os,nt,io, etc.) is one dictionary lookup away.The AST allowlist cannot detect this because no import statement executes.
2. Empty AST warnings are treated as trusted
execute_output()interprets:as authorization to invoke:
which selects the force-mode environment.
As a result, the least-scrutinized code executes in the most-privileged environment, the opposite of the intended security model.
Attack Scenario
Surface 1 — Local process with install token
Any local process reads:
from:
%APPDATA%/OpenSwarm/data/~/Library/Application Support/OpenSwarm/data/~/.local/share/OpenSwarm/data/and submits a crafted Output to:
Surface 2 — Persist then execute
Create an Output whose
backend.pyuses thesys.modulestechnique and execute it.Surface 3 — Indirect prompt injection
Poisoned content retrieved through MCP tools (Gmail, Drive, Web, etc.) instructs the LLM to generate an Output containing the bypass payload.
Since the AST gate remains silent, no approval dialog is shown.
Proof of Concept
All PoCs were executed against the current
backend/apps/outputs/executor.py.Marker files were created inside the OS temporary directory and removed afterward.
No data left the machine.
PoC A — Arbitrary file read (
skip_validation=False)Result:
{'read': '"""Per-install bearer token gating the localhost API...'}PoC B — Arbitrary file overwrite
Victim file afterwards:
PoC C — Command execution on
/executeResult:
{'rc': 0}Marker file:
Impact
Confidentiality (Critical)
auth.tokenIntegrity (Critical)
~/.bashrc, startup scripts, maliciousrun.sh, etc.)Availability (High)
Sandbox Escape
Steps To Reproduce
backend.pycontains PoC C.backend_result.rc == 0Technical Evidence
executor.py(L181–198)sysandsys.modulesremain fully accessible.outputs.py(L722–730)Empty warnings invoke the executor with the force-mode environment.
Suggested Remediation
P0 — Remove module access in the child preamble
Capture any required
io,json, orsys.stdoutreferences before removingsys.Add a self-test verifying:
raises inside the child.
P0 — Decouple AST warnings from force-mode execution
execute_output()should not invoke:merely because the warning list is empty.
Only explicit user approval (
force=True) should enable the force-mode environment.P1 — Replace AST filtering with real isolation
Treat AST validation as a usability feature rather than a security boundary.
Execute backend code inside OS-enforced isolation:
--network=noneDrop
PATHand jail the working directory.References
sys.modules— https://docs.python.org/3/library/sys.html#sys.modules