# Defense-in-Depth Score: Dify

**Repo:** https://github.com/langgenius/dify · **Commit:** `8222397ea989a15c305ee60d2e4b867883638f37` · **Reviewed:** 2026-10-03
**What it is:** Low-code platform for agentic workflows and RAG pipelines
**Category:** Agent Frameworks
**Scored configuration:** Self-hosted Docker Compose stack as shipped (docker-compose-template.yaml with docker/.env.example, images 1.17.1): classic agent and workflow apps plus Agent V2 with the default local shell sandbox.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory yes · untrusted input yes · third party extensions yes · sub agents yes · external communication yes

## Score: 2.9 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L1 | L1 | L0 | L0 | 0.15 | — | **0.15** | Medium |
| C2 | Approval gates | L0 | L0 | L0 | L0 | 0.00 | C2-POWERBYPASS | **0.00** | High |
| C3 | Tool & action scoping | L2 | L2 | L2 | L1 | 0.45 | — | **0.45** | High |
| C4 | Code-execution isolation | L4 | L3 | L0 | L0 | 0.53 | G1 | **0.50** (alt) | High |
| C5 | Untrusted input blast radius | L1 | L1 | L1 | L0 | 0.20 | C5-WORSTCASE | **0.20** | High |
| C6 | Memory, context & configuration integrity | L1 | L1 | L2 | L0 | 0.25 | — | **0.25** | Medium |
| C7 | Third-party extensions | L0 | L1 | L0 | L0 | 0.07 | C7-RCELOAD | **0.07** | Medium |
| C8 | Secrets & sensitive-data protection | L1 | L1 | L1 | L0 | 0.20 | — | **0.20** | High |
| C9 | Audit & traceability | L2 | L3 | L3 | L2 | 0.62 | — | **0.62** | High |
| C10 | Limits & kill switch | L2 | L2 | L2 | L1 | 0.45 | — | **0.45** | High |


Dify as shipped runs model-chosen tool calls, including an Agent V2 shell with public internet access and the agent's secret environment variables, with no human approval anywhere. Service-to-service authentication in the default deployment is not locked down, and the shared shell sandbox can reach the agent backend. Credential storage, SSRF filtering, and audit records are solid; harden service credentials and isolate the shell before exposing published apps to untrusted users.

## Critical gaps
- The Agent V2 shell runs arbitrary model-written scripts with no approval and is enabled by default. (ASI02, ASI09, T2; C2) — [dify-agent/src/dify_agent/layers/shell/layer.py:261](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L261); [api/configs/extra/agent_backend_config.py:65-71](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/extra/agent_backend_config.py#L65-L71)
- The shared local shell sandbox holds secret env vars, has public egress, and has a direct network route to the agent backend control plane. (ASI05, T11; C4) — [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081); [docker/ssrf_proxy/squid-agent.conf.template:18-22](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/ssrf_proxy/squid-agent.conf.template#L18-L22); [docker/docker-compose-template.yaml:1304-1311](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L1304-L1311)
- A hijacked agent can both exfiltrate (shell egress) and take irreversible tool actions with no human in the loop. (ASI01, LLM01, T6; C5) — [docker/ssrf_proxy/squid-agent.conf.template:18-22](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/ssrf_proxy/squid-agent.conf.template#L18-L22); [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081)
- The default shell prompt has the model install and run arbitrary public packages and MCP servers without consent, inside a sandbox holding the agent's secrets. (ASI04, T17, LLM03; C7) — [dify-agent/src/dify_agent/layers/shell/layer.py:123-135](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L123-L135); [dify-agent/src/dify_agent/layers/shell/layer.py:303-313](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L303-L313)

## Criterion details

### C1 Identity & least privilege — 0.15 (medium)

Agents and workflows act with workspace-level provider and tool credentials that any member can configure and that every end user of a published app then uses, with no per-user authorization. Service-to-service calls between the API, the plugin daemon, and the Agent V2 backend authenticate with single static keys, and those calls name the tenant in the request body; the default service credentials are also not locked down. The Agent V2 sandbox's own callback token is properly scoped by a signed execution context, but the sandbox can also reach the agent backend's control plane.

- **S L1:** Tools use dedicated per-workspace provider credentials stored once per tenant and shared by all callers; inner service auth is one static shared key that grants any tenant named in the payload. — [api/models/tools.py:94-100](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/models/tools.py#L94-L100); [api/controllers/inner_api/wraps.py:86-89](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/controllers/inner_api/wraps.py#L86-L89); [api/controllers/inner_api/agent/files.py:110](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/controllers/inner_api/agent/files.py#L110) (verified)
  - *To reach the next level:* No role- or capability-scoped credentials: read and write tools share whatever key the builder stored, and the inner API key is not tenant-scoped.
- **C L1:** The sandbox callback path is scoped by a signed execution context, but the plugin and agent inner API trusts any tenant_id the caller supplies, and the agent backend control plane takes a caller-supplied tenant in the run composition. — [dify-agent/src/dify_agent/agent_stub/server/agent_stub_config.py:68](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/agent_stub/server/agent_stub_config.py#L68); [api/controllers/inner_api/agent/files.py:110](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/controllers/inner_api/agent/files.py#L110); [dify-agent/src/dify_agent/layers/execution_context/configs.py:46](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/execution_context/configs.py#L46); [dify-agent/src/dify_agent/server/app.py:152-153](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/server/app.py#L152-L153) (verified)
  - *To reach the next level:* Inner API and agent backend control plane do not authorize against the requesting principal; only the sandbox stub path does.
- **D L0:** Default service credentials are not locked down. (verified)
  - *To reach the next level:* Least privilege requires manual hardening of service credentials.
- **B L0:** A holder of the inner API key or agent API token can drive tools, knowledge bases, and model calls in any tenant, and the Agent V2 sandbox has a direct network route to the agent backend (compose comment calls it a known limitation). — [docker/docker-compose-template.yaml:1304-1311](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L1304-L1311); [dify-agent/src/dify_agent/server/app.py:152-153](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/server/app.py#L152-L153); [dify-agent/src/dify_agent/layers/execution_context/configs.py:46](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/execution_context/configs.py#L46) (inferred)
  - *To reach the next level:* Blast radius is cross-tenant across every stored provider and tool credential; nothing confines a stolen key to one workspace.
- **Cap:** none
- **Notes:** Published web apps are reachable by anyone with the link in the community edition (web app auth is an enterprise feature), so end users act with the builder's credentials. Tenant separation was not confirmed to be a strict boundary.

### C2 Approval gates — 0.00 (high)

No tool call is gated by a human by default. The classic agent runner, the workflow tool node, and the Agent V2 runtime all execute model-chosen tool calls directly, including the Agent V2 shell, which is enabled by default and runs arbitrary scripts with internet access. Workflows offer a Human Input node and Agent V2 offers an ask-human tool, but the first is a step a builder may place in a workflow and the second is a tool the model decides whether to call; neither gates the agent's actions. Consequential actions (third-party write APIs, outbound HTTP, shell) happen without a person seeing them.

- **S L0:** Tool calls go straight from the agent loop to the tool implementation with no approval step. — [api/core/tools/tool_engine.py:86](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/tools/tool_engine.py#L86); searched `rg -n -S -i 'approv|confirm'` in `api/core/tools api/core/agent dify-agent/src/dify_agent/layers/shell dify-agent/src/dify_agent/layers/dify_plugin` → 0 hits (No approval or confirmation logic anywhere in the tool engine, classic agent runners, Agent V2 shell layer, or plugin tool layer.) (verified)
  - *To reach the next level:* No per-call approval showing the exact call.
- **C L0:** The most powerful tool, shell_run, runs the model's script directly. — [dify-agent/src/dify_agent/layers/shell/layer.py:261](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L261); [dify-agent/src/dify_agent/layers/shell/layer.py:303-313](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L303-L313) (verified)
  - *To reach the next level:* The shell and every write-capable tool would need to traverse an approval gate.
- **D L0:** There is no approval mode to turn on; the shell layer is injected by default. — [api/configs/extra/agent_backend_config.py:65-71](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/extra/agent_backend_config.py#L65-L71); [docker/.env.example:155](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/.env.example#L155) (verified)
  - *To reach the next level:* Approval would need to exist and be on by default.
- **B L0:** Unapproved actions include arbitrary outbound requests from the shell to the public internet and any write API a configured tool exposes, with no undo. — [docker/ssrf_proxy/squid-agent.conf.template:18-22](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/ssrf_proxy/squid-agent.conf.template#L18-L22); [dify-agent/src/dify_agent/layers/shell/layer.py:303-313](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L303-L313) (verified)
  - *To reach the next level:* No checkpoints, previews, or quantity bounds on external actions.
- **Cap:** C2-POWERBYPASS — The Agent V2 shell, the most powerful action path, is on by default and has no gate.
- **Notes:** Workflow Human Input nodes (api/core/workflow/nodes/human_input) are a real deterministic pause a builder can insert, but they are a workflow design element, not a gate on agent tool calls, so they were not scored as an alternative mechanism.

### C3 Tool & action scoping — 0.45 (high)

Tool arguments are cast to their declared types, and outbound HTTP from OpenAPI tools, MCP clients, and the HTTP node goes through a Squid proxy that blocks private and metadata addresses on every hop, including redirects. But the toolset is general by design: an arbitrary-URL HTTP node, custom OpenAPI tools against any host, and an Agent V2 shell that takes raw scripts. Classic agent apps only receive tools the builder selected, while Agent V2 adds the shell by default. A misused tool can reach any public host with the workspace's credentials.

- **S L2:** Typed parameter casting plus network-level SSRF filtering via the proxy, but the shell and HTTP tools accept arbitrary scripts and URLs. — [api/core/tools/__base/tool.py:88-96](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/tools/__base/tool.py#L88-L96); [api/core/tools/custom_tool/tool.py:283](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/tools/custom_tool/tool.py#L283); [docker/ssrf_proxy/squid.conf.template:13-15](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/ssrf_proxy/squid.conf.template#L13-L15) (verified)
  - *To reach the next level:* Host allowlists and narrow tools instead of raw shell and arbitrary-URL requests.
- **C L2:** Built-in, OpenAPI, and MCP HTTP traffic goes through the SSRF proxy; the plugin daemon also sits on the unrestricted default network, so plugin tools are not covered. — [api/core/tools/custom_tool/tool.py:283](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/tools/custom_tool/tool.py#L283); [docker/docker-compose-template.yaml:567-586](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L567-L586) (verified)
  - *To reach the next level:* A shared validation layer covering plugin and shell tools.
- **D L2:** Builders choose tools per app, but Agent V2 injects the shell (write and exec) by default. — [api/configs/extra/agent_backend_config.py:65-71](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/extra/agent_backend_config.py#L65-L71) (verified)
  - *To reach the next level:* A read-only default tool set with write and exec opt-in.
- **B L1:** The shell reaches the whole public internet and tools carry workspace credentials; private ranges are blocked. — [docker/ssrf_proxy/squid-agent.conf.template:18-22](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/ssrf_proxy/squid-agent.conf.template#L18-L22); [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081) (verified)
  - *To reach the next level:* Scope tools to a project with quantity bounds.
- **Cap:** none

### C4 Code-execution isolation — 0.50 (high)

Model-driven code runs outside the API process: workflow code and template nodes go to the separate dify-sandbox service, and the Agent V2 shell runs in a dedicated non-root container with Landlock filesystem rules per job. That container is shared by every workspace on the deployment, its Landlock layer silently falls back to no isolation on unsupported kernels, and its policy configuration is not integrity-protected. Inside, the shell holds the agent's secret environment variables and a callback token, has public internet egress, and has a direct network route to the agent backend. An opt-in E2B backend moves the shell to a remote sandbox service.

- **default configuration** (default; raw 0.47 → 0.47)
  - **S L2:** A stock container running as a non-root user with Landlock V1 filesystem rules; compose sets no seccomp profile, capability drop, no-new-privileges, or read-only root filesystem. — [dify-agent-runtime/docker/Dockerfile:74-79](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent-runtime/docker/Dockerfile#L74-L79); [docker/docker-compose-template.yaml:542-564](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L542-L564) (verified)
    - *To reach the next level:* Hardened container profile (dropped capabilities, no-new-privileges, seccomp, read-only root) with network denied by default.
  - **C L3:** Every model-reachable execution path found goes to a sandbox: code and Jinja nodes to dify-sandbox, shell and CLI bootstraps to local_sandbox. — [api/core/helper/code_executor/code_executor.py:19](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/helper/code_executor/code_executor.py#L19); [dify-agent/src/dify_agent/layers/shell/layer.py:303-313](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L303-L313); [dify-agent-runtime/cmd/runner/main.go:312-315](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent-runtime/cmd/runner/main.go#L312-L315) (verified)
    - *To reach the next level:* Fail closed: Landlock setup failure falls back to running without filesystem isolation.
  - **D L2:** Isolation is on by default but Landlock degrades silently, and its policy configuration is not integrity-protected. — [dify-agent-runtime/cmd/runner/main.go:312-315](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent-runtime/cmd/runner/main.go#L312-L315) (verified)
    - *To reach the next level:* Sandbox policy should be defined outside anything the agent can influence, and escalation should need a human.
  - **B L0:** Credentials sit in the sandbox environment (configured secrets and the stub token), egress to the public internet is open, and the container shares a network with the agent backend control plane. — [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081); [dify-agent/src/dify_agent/layers/shell/layer.py:303-313](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L303-L313); [docker/ssrf_proxy/squid-agent.conf.template:18-22](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/ssrf_proxy/squid-agent.conf.template#L18-L22); [docker/docker-compose-template.yaml:1304-1311](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L1304-L1311) (verified)
    - *To reach the next level:* No secrets in the sandbox, egress off or allowlisted, and no route to control-plane services.
- **opt-in E2B remote sandbox backend** (alt; raw 0.53, cap G1 → 0.50) ← counted
  - **S L4:** Shell jobs run in remote E2B sandboxes created per binding. — [dify-agent/src/dify_agent/runtime_backend/e2b.py:151-155](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/runtime_backend/e2b.py#L151-L155) (verified)
  - **C L3:** With the E2B backend selected, all shell runtime leases come from E2B. — [docker/docker-compose.e2b.yaml:27-30](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose.e2b.yaml#L27-L30); [dify-agent/src/dify_agent/runtime_backend/e2b.py:151-155](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/runtime_backend/e2b.py#L151-L155) (verified)
    - *To reach the next level:* Fail-closed behaviour on sandbox creation errors was not verified end to end.
  - **D L0:** Off by default; the shipped backend is local. — [docker/.env.example:270](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/.env.example#L270) (verified)
    - *To reach the next level:* Make the remote sandbox the default.
  - **B L0:** The same secret environment variables and stub token are injected into the remote sandbox. — [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081); [dify-agent/src/dify_agent/layers/shell/layer.py:303-313](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L303-L313) (verified)
    - *To reach the next level:* Keep secrets out of the sandbox environment.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.
- **Notes:** dify-sandbox (code node executor) is an external image; its isolation claims were not verified here and it runs with ENABLE_NETWORK=true via the SSRF proxy. The shared local_sandbox means Landlock is the only per-tenant filesystem boundary.

### C5 Untrusted input blast radius — 0.20 (high)

Nothing structurally limits a hijacked agent. Untrusted content arrives from public web-app users, crawled web pages, uploaded documents, tool and MCP results, and shell output, and enters the model's context with no provenance marking beyond plain output tags on shell results. The same session can hold workspace secrets in the shell environment and has open egress through the shell and HTTP tools. A successful injection can therefore leak data and take irreversible actions with no human involved.

- **S L1:** Shell output is wrapped in <output> tags; nothing acts on the wrapper and no injection handling exists elsewhere. — [dify-agent/src/dify_agent/layers/shell/layer.py:735-737](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L735-L737); searched `rg -n -S -i 'prompt.injection|untrusted|jailbreak'` in `api/core/agent api/core/tools dify-agent/src/dify_agent/layers` → 0 hits (No prompt-injection handling, untrusted-content marking, or taint tracking in the agent, tool, or Agent V2 layer code.) (verified)
  - *To reach the next level:* Disable or gate egress and state-changing tools once untrusted content is in context.
- **C L1:** Only the shell path has a delimiter; web pages, documents, MCP and plugin results are not distinguished. — [dify-agent/src/dify_agent/layers/shell/layer.py:735-737](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L735-L737); searched `rg -n -S -i 'prompt.injection|untrusted|jailbreak'` in `api/core/agent api/core/tools dify-agent/src/dify_agent/layers` → 0 hits (No prompt-injection handling, untrusted-content marking, or taint tracking in the agent, tool, or Agent V2 layer code.) (verified)
  - *To reach the next level:* Cover tool results, retrieved documents, and MCP outputs.
- **D L1:** The shell output wrapper is always on but is a formatting convention, not a control. — [dify-agent/src/dify_agent/layers/shell/layer.py:735-737](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L735-L737) (verified)
  - *To reach the next level:* A control that is on and warned when disabled.
- **B L0:** Leak via shell egress plus irreversible writes through tools, unattended; untrusted sources include public web-app chat and crawled sites. — [docker/ssrf_proxy/squid-agent.conf.template:18-22](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/ssrf_proxy/squid-agent.conf.template#L18-L22); [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081); [docker/.env.example:151-152](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/.env.example#L151-L152) (verified)
  - *To reach the next level:* Rule of Two enforcement so a session with untrusted input loses egress or state change.
- **Cap:** C5-WORSTCASE — Exfiltration and irreversible action are both possible unattended in the default configuration.

### C6 Memory, context & configuration integrity — 0.25 (medium)

Conversation history is scoped to a conversation and knowledge-base retrieval is filtered by workspace in queries. Agent V2 lets the model itself push persistent changes to its own agent configuration (skills, notes, files, and environment variables) through the sandbox CLI, limited to build-draft sessions by a signed context; those changes load into later runs and reach end users once a builder publishes. Knowledge bases filled from crawled websites or uploaded documents are retrieved for every user of an app with no provenance or review step.

- **S L1:** Model writes to persistent agent config are restricted to build drafts but not validated. — [api/services/agent_config_service.py:825-829](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/services/agent_config_service.py#L825-L829); [api/services/agent_config_service.py:968-983](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/services/agent_config_service.py#L968-L983); [dify-agent/src/dify_agent/layers/config/layer.py:31-33](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/config/layer.py#L31-L33) (verified)
  - *To reach the next level:* Gate model-originated config writes behind human review.
- **C L1:** Only the agent-config path has a write restriction; knowledge bases and conversation history have none. — [api/services/agent_config_service.py:968-983](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/services/agent_config_service.py#L968-L983); [docker/.env.example:151-152](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/.env.example#L151-L152) (verified)
  - *To reach the next level:* Controls over knowledge-base ingestion and retrieval provenance.
- **D L2:** Datasets and conversation history are filtered by tenant and conversation in queries. — [api/core/rag/retrieval/dataset_retrieval.py:701](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/rag/retrieval/dataset_retrieval.py#L701); [api/core/memory/token_buffer_memory.py:218](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/memory/token_buffer_memory.py#L218) (verified)
  - *To reach the next level:* Namespace guarantees the model cannot alter, with retention on by default.
- **B L0:** Poisoned knowledge-base content persists and is retrieved for every user of an app, where it can drive tool use; poisoned draft config reaches all users once published. — [api/core/rag/retrieval/dataset_retrieval.py:701](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/rag/retrieval/dataset_retrieval.py#L701); [api/services/agent_config_service.py:825-829](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/services/agent_config_service.py#L825-L829) (inferred)
  - *To reach the next level:* Session-scoped persistence or human-reviewed, rollbackable memory.
- **Cap:** none

### C7 Third-party extensions — 0.07 (medium)

Marketplace plugins are installed by pinned unique identifiers and the plugin daemon ships with signature verification forced on, but any workspace member may install plugins by default and patch updates are applied automatically without re-approval. Plugins run inside the plugin daemon container, which holds database and storage credentials and an unrestricted network route. Separately, the Agent V2 shell prompt tells the model to install whatever Python or Node packages and MCP servers it needs, and it does so without asking, inside a sandbox holding the agent's secrets.

- **S L0:** The default Agent V2 prompt directs model-chosen package and MCP server installs from public registries. — [dify-agent/src/dify_agent/layers/shell/layer.py:123-135](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L123-L135) (verified)
  - *To reach the next level:* Pinned, integrity-checked or allowlisted sources for runtime installs.
- **C L1:** Plugins are signature-verified by default configuration; model-installed packages are not verified at all. — [docker/docker-compose-template.yaml:599](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L599); [dify-agent/src/dify_agent/layers/shell/layer.py:123-135](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L123-L135) (verified)
  - *To reach the next level:* Verification for every extension type, including sandbox installs.
- **D L0:** Model installs need no consent; plugin installs are open to every member and auto-upgrade on fixes by default. — [dify-agent/src/dify_agent/layers/shell/layer.py:123-135](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L123-L135); [api/models/account.py:315-319](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/models/account.py#L315-L319); [api/services/plugin/plugin_auto_upgrade_service.py:41-45](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/services/plugin/plugin_auto_upgrade_service.py#L41-L45) (verified)
  - *To reach the next level:* Nothing third-party enabled without an explicit admin decision showing what will run.
- **B L0:** Model-installed code runs in the shell with its secret env and stub token; plugins run in the daemon container that carries DB credentials. — [dify-agent/src/dify_agent/layers/shell/layer.py:303-313](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L303-L313); [docker/docker-compose-template.yaml:567-586](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L567-L586) (inferred)
  - *To reach the next level:* Per-extension sandboxes with scrubbed environments and scoped credentials.
- **Cap:** C7-RCELOAD — By default the Agent V2 shell installs and runs model-chosen packages from public registries without consent.
- **Notes:** Plugin signature enforcement and plugin process confinement live in the external dify-plugin-daemon and were not verified here.

### C8 Secrets & sensitive-data protection — 0.20 (high)

Stored provider and tool credentials are encrypted at rest with per-tenant RSA keys and masked in console responses. But default service credentials are not locked down, the Agent V2 shell receives configured secret values as environment variables while the prompt tells the model they are there, and only the callback token is redacted from shell output unless the operator adds patterns. Anonymous, content-free usage telemetry is on by default.

- **S L1:** Encryption at rest and response masking exist, but service credential defaults are not locked down and secrets are placed where the model can read them. — [api/configs/middleware/__init__.py:94-99](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/middleware/__init__.py#L94-L99); [api/core/helper/encrypter.py:5-10](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/helper/encrypter.py#L5-L10); [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081) (verified)
  - *To reach the next level:* Harden service credential defaults and keep credential values out of model-reachable environments.
- **C L1:** Masking covers console responses; shell output redaction only removes the stub token by default. — [dify-agent/src/dify_agent/layers/shell/layer.py:570-590](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L570-L590); [docker/envs/core-services/dify-agent.env.example:123](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/envs/core-services/dify-agent.env.example#L123) (verified)
  - *To reach the next level:* Redaction on model-bound tool output and subprocess environments.
- **D L1:** Community telemetry is on by default and content-free (instance id, version, OS); Sentry is opt-in. — [api/configs/feature/__init__.py:870-881](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/feature/__init__.py#L870-L881); [api/services/telemetry_service.py:86-96](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/services/telemetry_service.py#L86-L96) (verified)
  - *To reach the next level:* Telemetry opt-in.
- **B L0:** Long-lived third-party API keys configured as agent secrets are readable by the model through the shell environment. — [api/core/workflow/nodes/agent_v2/runtime_request_builder.py:1078-1081](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/agent_v2/runtime_request_builder.py#L1078-L1081); [dify-agent/src/dify_agent/layers/shell/layer.py:163](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/layers/shell/layer.py#L163) (verified)
  - *To reach the next level:* Scoped, short-lived credentials substituted at execution time.
- **Cap:** none

### C9 Audit & traceability — 0.62 (high)

Dify records every tool call in the database: classic agent thoughts store the tool, its input, and its observation; Agent V2 tool calls are relayed back and stored the same way; workflow node executions are persisted by default; and Human Input form submissions are recorded. Records are written by the API server, which the sandboxed shell cannot reach. The record is not tamper-evident, actor attribution is partial (classic agent thoughts are always stamped as account-created), and there is no correlation chain across nested workflows.

- **S L2:** Structured per-call records with inputs and outputs. — [api/models/model.py:2443-2453](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/models/model.py#L2443-L2453); [api/core/agent/base_agent_runner.py:256](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/agent/base_agent_runner.py#L256) (verified)
  - *To reach the next level:* Full actor attribution and correlation IDs across nested workflows and agents.
- **C L3:** Classic, workflow, and Agent V2 tool calls, plugin and MCP tools via the same engine, and human input submissions are all recorded. — [api/core/tools/tool_engine.py:86](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/tools/tool_engine.py#L86); [api/core/app/apps/agent_app/app_runner.py:418-433](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/app/apps/agent_app/app_runner.py#L418-L433); [api/core/workflow/nodes/human_input/boundary.py:5](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/nodes/human_input/boundary.py#L5) (verified)
  - *To reach the next level:* Config changes, memory writes, and credential use are not all audited.
- **D L3:** On by default, written by the API to its database through the default SQLAlchemy repository. — [api/configs/feature/__init__.py:1002-1006](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/feature/__init__.py#L1002-L1006); [api/models/model.py:2443-2453](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/models/model.py#L2443-L2453) (verified)
  - *To reach the next level:* Disabling or purging should itself be logged at operator level.
- **B L2:** Records are written per action in the normal path; failures surface as errors but actions are not blocked on record writes. — [api/core/app/apps/agent_app/app_runner.py:418-433](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/app/apps/agent_app/app_runner.py#L418-L433) (verified)
  - *To reach the next level:* Durable, replayable trajectory with fail-closed recording for high-risk actions.
- **Cap:** none

### C10 Limits & kill switch — 0.45 (high)

Classic agents are capped at 99 iterations, workflows at 500 steps and one hour, and Agent V2 runs at 500 model requests and one hour, with per-call timeouts for the code sandbox and plugins. There is no token or cost cap, per-app concurrency is unlimited by default, and stopping a run sets a flag the loop checks between steps. Nested workflows called as tools get their own budgets, bounded only by a call depth of five.

- **S L2:** Iteration and step caps plus wall-clock limits and per-execution timeouts; no token or cost cap; halt is a cooperative flag. — [api/core/agent/fc_agent_runner.py:122](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/agent/fc_agent_runner.py#L122); [api/core/workflow/workflow_entry.py:178-179](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/workflow/workflow_entry.py#L178-L179); [dify-agent/src/dify_agent/runtime/runner.py:97-98](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/runtime/runner.py#L97-L98); [api/core/app/apps/base_app_queue_manager.py:220](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/core/app/apps/base_app_queue_manager.py#L220) (verified)
  - *To reach the next level:* Token or cost caps and rate limits on side-effecting tools.
- **C L2:** Top-level loops plus tool timeouts; nested workflows start fresh limits. — [api/configs/feature/__init__.py:921-939](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/feature/__init__.py#L921-L939); [docker/docker-compose-template.yaml:601](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/docker/docker-compose-template.yaml#L601) (verified)
  - *To reach the next level:* Nested workflows and background shell jobs should count against the parent's budget.
- **D L2:** Sensible defaults that operators configure; the model cannot raise them. — [api/configs/feature/__init__.py:921-939](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/feature/__init__.py#L921-L939); [dify-agent/src/dify_agent/runtime/runner.py:97-98](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/runtime/runner.py#L97-L98) (verified)
  - *To reach the next level:* Hard ceilings that configuration cannot exceed and no budget reset through delegation.
- **B L1:** One-hour ceilings, unlimited spend, and unlimited concurrent requests per app by default. — [api/configs/feature/__init__.py:93-96](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/api/configs/feature/__init__.py#L93-L96); [dify-agent/src/dify_agent/runtime/runner.py:97-98](https://github.com/langgenius/dify/blob/8222397ea989a15c305ee60d2e4b867883638f37/dify-agent/src/dify_agent/runtime/runner.py#L97-L98) (verified)
  - *To reach the next level:* Tight per-run time and cost ceilings with cancellation of pending calls.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: public web-app chat, crawled sites and uploaded documents, tool/MCP results, shell output (docker/.env.example:151-152, layers/shell/layer.py:303-313) · [B] sensitive data/systems: workspace secrets injected into the shell env and tenant knowledge bases (agent_v2/runtime_request_builder.py:1078-1081) · [C] state change / egress: shell with public egress (squid-agent.conf.template:22), HTTP/OpenAPI tools · Same default session? Yes

## Highest-impact improvements
1. Harden service-to-service credentials and scope the inner API to the calling component's tenant. — C1 D L0→L2, +0.100 before caps (Playbook 4)
2. Remove local_sandbox from the agent backend network, require a shellctl token, keep secrets out of the sandbox env, and default sandbox egress to an allowlist. — C4 B L0→L2, +0.100 before caps (Playbook 3)
3. Harden sandbox policy configuration and make Landlock setup failure fatal. — C4 D L2→L3, +0.050 before caps (Playbook 3)
4. Add per-call human approval showing the exact script or request for shell_run and write-capable tools in published apps, on by default. — C2 S L0→L3, +0.225 before caps (Playbook 5)
5. Stop prompting the model to install arbitrary packages and MCP servers; allow only pinned, allowlisted installs declared by the builder. — C7 S L0→L2, +0.150 before caps (Playbook 3)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- External components that enforce key controls were not examined: dify-sandbox (code node executor), dify-plugin-daemon (plugin signature checks and process confinement), and graphon (workflow engine limits).
- One cross-tenant path in C1 B and knowledge-base poisoning reach (C6 B) are inferred from verified individual links, not demonstrated.
- Enterprise-only features (web app access control, Azure Key Vault key provider, enterprise sandbox gateway) were not scored; the community-edition defaults were.
- The clone used a sparse checkout excluding web/i18n and web/public; the web UI was reviewed only for markdown rendering.
- No text attempting to steer AI reviewers was found in the repository.
