langgenius/ dify
AgentBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.
Trust score
Scanned Oct. 14th, 2026 at f77facf against 34 rules · How scoring works
What pulled the score down
- HighCORS / Origin / Host Validation (DNS Rebinding Defense) ATR-M0003
- HighTransport Security: TLS Everywhere (Remote) ATR-M0009
- HighUnrestricted Network Fetch / SSRF ATR-C0001
- HighStatic Secrets ATR-C0002
- MediumLogging Hygiene (No Secrets/PII/Tokens) & Audit Trail ATR-C0005
- MediumContainer/Runtime Hardening ATR-C0006
- HighDependency Risk & Supply Chain ATR-C0007
- HighPath Traversal & Symlink Bypass (Filesystem Tools) ATR-C0011
- HighTool Output Sanitization & Injection Containment ATR-C0013
At a glance
- Repository-wide7 of 15
- mcp-server/api (MCP server)3 of 11
What to fix first
10 issues, most severe first. Each one points to the exact code, with the recommended fix and a prompt you can paste into your coding agent.
- 1HighUnrestricted Network Fetch / SSRFRepository-wide · ATR-C0001The outbound HTTP client used for custom tool invocations and HTTP Request nodes falls back to a direct httpx client whenever SSRF_PROXY_HTTP_URL/SSRF_PROXY_HTTPS_URL are unset (they default to None in api/core/helper/ssrf_proxy.py), meaning the SSRF-blocking Squid proxy is an opt-in deployment choice rather than a code-enforced control. The custom-tool invocation path (api/core/tools/custom_tool/tool.py) dispatches the tool's configured server URL through this client with no in-code destination gate (no check against private/loopback/link-local ranges) before the request is sent. A deployment that does not explicitly configure the SSRF proxy environment variables -- which is possible under the project's supported configuration surface, not just the shipped Compose example -- allows a custom tool's configured URL to reach internal or private network destinations unguarded.
self, credentials: dict[str, Any], parameters: dict[str, Any], format_only: bool = False ) -> str: """ validate the credentials for Api tool """ # assemble validate request and request parameters headers = self.assembling_request(parameters) if format_only: return "" response = self.do_http_request(self.api_bundle.server_url, self.api_bundle.method, headers, parameters)Show 2 more locations
SSRF_PROXY_ALL_URL: str | None = Field( description="Proxy URL for HTTP or HTTPS requests to prevent Server-Side Request Forgery (SSRF)", default=None, ) SSRF_PROXY_HTTP_URL: str | None = Field( description="Proxy URL for HTTP requests to prevent Server-Side Request Forgery (SSRF)", default=None, ) SSRF_PROXY_HTTPS_URL: str | None = Field( description="Proxy URL for HTTPS requests to prevent Server-Side Request Forgery (SSRF)", default=None, )def _build_ssrf_client(verify: bool) -> httpx.Client: if dify_config.SSRF_PROXY_ALL_URL: return httpx.Client( proxy=dify_config.SSRF_PROXY_ALL_URL, verify=verify, limits=_SSRF_CLIENT_LIMITS, ) if dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL: return httpx.Client( mounts=_create_proxy_mounts(verify=verify), verify=verify, limits=_SSRF_CLIENT_LIMITS, ) …+1 more location in the finding JSON
Recommended fixEnforce SSRF protection in code rather than relying solely on an external proxy default: add a destination-validation check (reject private/loopback/link-local IP ranges) directly in the HTTP client construction path in api/core/helper/ssrf_proxy.py so protection applies even when SSRF_PROXY_HTTP_URL/HTTPS_URL are unset, and add an explicit gate in the custom-tool dispatch path in api/core/tools/custom_tool/tool.py before the configured server URL is fetched.
- 2HighStatic SecretsRepository-wide · ATR-C0002docker-compose.yaml and docker-compose-template.yaml ship REAL-LOOKING hardcoded default secret VALUES for inter-service authentication that take effect the moment an operator runs
docker compose upwithout overriding the env vars: PLUGIN_DAEMON_KEY/PLUGIN_DIFY_INNER_API_KEY default to the fixed base64-looking string 'lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi' / 'QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1' (identical across every public clone of the repo), DIFY_AGENT_SERVER_SECRET_KEY defaults to the fixed hex string '0123456789abcdef0123456789abcdef', and MINIO_SECRET_KEY defaults to 'minioadmin'. These are not placeholder/empty values in a .env.example (which would be the negative case) -- they are live fallback defaults baked directly into the shipped docker-compose YAML that is the project's documented one-command quick-start path, so any internet-reachable deployment that forgets to override them authenticates its internal plugin-daemon/inner-API channel and MinIO object storage with a publicly known static secret. This matches the rule's positive_indicator 'default changeme-style value' and its CWE-798/312 class directly.SERVER_KEY: ${PLUGIN_DAEMON_KEY:-lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi} ... DIFY_INNER_API_KEY: ${PLUGIN_DIFY_INNER_API_KEY:[REDACTED]}Show 2 more locations
DIFY_AGENT_SERVER_SECRET_KEY: ${DIFY_AGENT_SERVER_SECRET_KEY:[REDACTED]}MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}+37 more locations in the finding JSON
Recommended fixRemove hardcoded fallback values from docker-compose for PLUGIN_DAEMON_KEY/PLUGIN_DIFY_INNER_API_KEY/DIFY_AGENT_SERVER_SECRET_KEY/MINIO_SECRET_KEY; instead fail fast (or auto-generate a random secret at first-run via an init script) when the operator has not supplied a value, so no installation ever runs with a secret value that is identical and publicly known across every Dify deployment.
- 3HighDependency Risk & Supply ChainRepository-wide · ATR-C0007Trivy's scan of the committed lockfiles (api/uv.lock, dify-agent/uv.lock, pnpm-lock.yaml, dify-agent-runtime/go.mod) found 23 known CVEs in pinned, locked dependency versions, including chromadb==0.5.20 (pinned in api/uv.lock as the Chroma vector-store provider's declared runtime dependency) mapped to CVE-2026-45833 (CRITICAL), CVE-2026-45830 (HIGH), and CVE-2026-45831 (HIGH) with no fixed version yet published, alongside other HIGH/MEDIUM findings (e.g. pydantic-ai-slim, golang.org/x/mod, socket.io-parser) that do have published fixed versions the project has not yet adopted. The presence of these scanner-confirmed, unpatched CVEs in exact pinned dependency versions constitutes a supply-chain risk on its own; this finding does not assert a specific demonstrated remote-exploit path through the Chroma vector-store API, only that the vulnerable versions are locked and declared as runtime dependencies.
name = "chromadb" version = "0.5.20" source = { registry = "https://pypi.org/simple" } dependencies = [Show 2 more locations
(go.mod pins golang.org/x/mod to a version affected by CVE-2026-56864 HIGH / CVE-2026-56865 HIGH, per trivy-fs.json Results[] for Target=='dify-agent-runtime/go.mod')
"preinstall": "npx only-allow pnpm",
+14 more locations in the finding JSON
Recommended fixUpgrade or replace chromadb beyond 0.5.20 once a patched release addressing CVE-2026-45833/45830/45831 is available (or pin to a patched fork / disable the Chroma vector-store provider until then); separately upgrade pydantic-ai-slim to >=2.53.0, bump the vulnerable golang.org/x/mod dependency, and update socket.io-parser to a fixed release; add a scheduled dependency CVE scan to CI to catch newly disclosed CVEs in already-pinned packages.
- 4HighPath Traversal & Symlink Bypass (Filesystem Tools)Repository-wide · ATR-C0011The agent CLI's skill-pull flow (dify-agent-runtime/internal/agentcli/config.go) builds the download archive path and extraction directory directly from a skill name supplied via CLI argument or a downloaded manifest, using filepath.Join(targetDir, name+".zip") and a matching skillDir, without checking for path separators, absolute paths, or containment within targetDir. filepath.Join normalizes '../' segments, so a name such as '../../outside' causes the resulting path to escape targetDir, and the subsequent MkdirAll/os.WriteFile calls can then create or overwrite files outside the intended skills directory. The earlier SECURE verdict considered only the UUID-keyed API upload path (which is safely indirected) and did not examine this separate CLI/manifest-driven skill-pull flow, which has no equivalent indirection or validation before the filesystem sink.
// RunConfigSkillsPull executes the `config skills pull` command. func RunConfigSkillsPull(env *Environment, names []string, localDir string, jsonOutput bool) error { client, err := NewStubClient(env) if err != nil { return err } defer func() { _ = client.Close() }()Show 2 more locations
} `json:"items"` } `json:"skills"` } if err := json.Unmarshal(body, &manifest); err != nil { return fmt.Errorf("parse manifest: %w", err) } for _, item := range manifest.Skills.Items { names = append(names, item.Name)} err = runConfigPulls(len(names), func(index int) error { name := names[index] download, err := client.CreateConfigDownloadURL(ctx, "skill", name) if err != nil { return fmt.Errorf("request config skill %q download URL: %w", name, err) } archiveBytes, err := client.DownloadFromURL(download.DownloadURL) if err != nil { return fmt.Errorf("download config skill %q: %w", name, err) } archivePath := filepath.Join(targetDir, name+".zip") …+16 more locations in the finding JSON
Recommended fixValidate the skill name before it is used to build any filesystem path in config.go's skill-pull flow: reject names containing path separators or '..' segments, or that resolve (after filepath.Clean/filepath.Abs) outside targetDir, before calling filepath.Join/MkdirAll/os.WriteFile.
- 5HighTool Output Sanitization & Injection ContainmentRepository-wide · ATR-C0013Dify's CoT agent runner assigns tool invocation results verbatim to scratchpad.observation and then interpolates that value verbatim into the 'Observation:' section of model-visible assistant history (api/core/agent/cot_agent_runner.py), with no sanitization, escaping, role-marker stripping, or untrusted-return annotation applied anywhere between the tool-output assignment and the point it re-enters the model's context. This allows tool-sourced content (e.g. a malicious web page or API response surfaced through a fetch/scrape-style tool) to inject text that the model treats as part of its own reasoning history, with no host-enforced trust boundary on that path.
tool_invoke_response, tool_invoke_meta = self._handle_invoke_action( session=session, action=scratchpad.action, tool_instances=tool_instances, message_file_ids=message_file_ids, trace_manager=trace_manager, ) scratchpad.observation = tool_invoke_response scratchpad.agent_response = tool_invoke_responseShow 1 more location
def _format_assistant_message(self, agent_scratchpad: list[AgentScratchpadUnit]) -> str: """ format assistant message """ message = "" for scratchpad in agent_scratchpad: if scratchpad.is_final(): message += f"Final Answer: {scratchpad.agent_response}" else: message += f"Thought: {scratchpad.thought}\n\n" if scratchpad.action_str: message += f"Action: {scratchpad.action_str}\n\n" if scratchpad.observation: message += f"Observation: {scratchpad.observation}\n\n"Recommended fixAdd a sanitization/escaping layer for tool outputs before they are concatenated into the agent scratchpad: strip or neutralize control characters and fake role/system markers, and wrap tool output in an explicit untrusted-content envelope before it is interpolated into the Observation: section in cot_agent_runner.py and fc_agent_runner.py.
Show the other 5 issues
- 6HighCORS / Origin / Host Validation (DNS Rebinding Defense)mcp-server/api (MCP server) · ATR-M0003The
mcpFlask blueprint that serves the remote streamable-HTTP MCP endpoint (POST /mcp/server/<server_code>/mcp) is registered in extensions/ext_blueprints.py WITHOUT any call to_apply_cors_once(mcp_bp, ...)-- unlike every other blueprint (service_api, openapi, web, console, files, trigger) which each get an explicit CORS policy. No Origin header validation exists anywhere in controllers/mcp/mcp.py or core/mcp/server/streamable_http.py either. The server binds beyond loopback (0.0.0.0:5001 in api/docker/entrypoint.sh / gunicorn --bind) and the exposed MCP tool invokes AppGenerateService.generate -- a host-side effect that runs the full Dify app (workflow/LLM calls, possible plugin/tool execution) scoped to that app's tenant. Missing Origin validation plus this host-side impact meets the rule's VULNERABLE bar.def init_app(app: DifyApp): ... _apply_cors_once(service_api_bp, ...) from controllers.console import bp as console_app_bp from controllers.files import bp as files_bp _apply_cors_once(openapi_bp, ...) from controllers.mcp import bp as mcp_bp _apply_cors_once(web_bp, ...) from controllers.service_api import bp as service_api_bp _apply_cors_once(console_app_bp, ...) from controllers.web import bp as web_bp _apply_cors_once(files_bp, ...) _apply_cors_once( service_api_bp, …Show 1 more location
@mcp_ns.route("/server/<string:server_code>/mcp") class MCPAppApi(Resource): @mcp_ns.expect(mcp_ns.models[MCPRequestPayload.__name__]) @mcp_ns.doc("handle_mcp_request") @mcp_ns.doc(description="Handle Model Context Protocol (MCP) requests for a specific server") @mcp_ns.doc(params={"server_code": "Unique identifier for the MCP server"}) @mcp_ns.response(200, "MCP JSON-RPC response", mcp_ns.models[MCPJSONRPCResponse.__name__]) @mcp_ns.response(202, "MCP notification accepted") @mcp_ns.response(400, "Invalid MCP request or parameters") @mcp_ns.response(404, "Server or app not found") def post(self, server_code: str):Recommended fixApply an explicit CORS/Origin allowlist policy to the mcp blueprint, and validate the Host/Origin header server-side consistent with the MCP Authorization spec's DNS-rebinding guidance.
- 7HighOAuth 2.1 + PKCE Correctness (MCP Authorization)mcp-server/api (MCP server) · ATR-M0005The app-published MCP server at
/mcp/server/<server_code>/mcpis a remote streamable-HTTP transport with NO OAuth 2.1 authorization layer at all: there is no/.well-known/oauth-protected-resourcemetadata endpoint, noAuthorization: Bearervalidation, and no PKCE flow for MCP clients connecting to this endpoint. The sole access control is knowledge of theserver_codepath segment (a 16-char CSPRNG string, see models/model.py / libs/helper.py generate_string, and repositories/app/mcp_server_repository.py _SERVER_CODE_LENGTH=16). Per the MCP Authorization spec (2025-06-18) a remote transport server is expected to implement OAuth 2.1; a bearer-style opaque-token-in-URL scheme with no token issuance/rotation/revocation flow, no audience binding, and no per-client consent is exactly the gap this rule targets. (Note: this repo DOES implement a full OAuth 2.1 + PKCE client flow in core/mcp/auth/auth_flow.py, but that code path is Dify acting as an MCP CLIENT connecting OUT to third-party MCP servers, not Dify's own MCP SERVER endpoint authenticating inbound MCP clients -- the two are architecturally distinct and this finding is about the inbound server side.)@mcp_ns.route("/server/<string:server_code>/mcp") class MCPAppApi(Resource): @mcp_ns.expect(mcp_ns.models[MCPRequestPayload.__name__]) @mcp_ns.doc("handle_mcp_request") @mcp_ns.doc(description="Handle Model Context Protocol (MCP) requests for a specific server") @mcp_ns.doc(params={"server_code": "Unique identifier for the MCP server"}) def post(self, server_code: str):Show 1 more location
server_code=AppMCPServer.generate_server_code(_SERVER_CODE_LENGTH, session=session),
Recommended fixImplement the MCP Authorization spec for this inbound server surface: publish OAuth 2.1 protected-resource metadata, require PKCE-backed bearer tokens scoped per-user/per-client with expiry and revocation, instead of relying solely on a static server_code value embedded in the URL.
- 8HighTransport Security: TLS Everywhere (Remote)mcp-server/api (MCP server) · ATR-M0009The API/gunicorn process serving the MCP endpoint speaks plain HTTP on its own -- the gunicorn invocation in api/docker/entrypoint.sh never loads a certificate/key or enables an SSL context, so TLS (when present at all) is solely the responsibility of whatever sits in front of this container. Any deployment that reaches this container's own listener directly (bare docker run, custom K8s Service, or a misconfigured proxy bypass) serves the MCP JSON-RPC endpoint, including app credentials and conversation content, over plaintext HTTP.
exec gunicorn \ --no-control-socket \ --bind "${DIFY_BIND_ADDRESS:-0.0.0.0}:${DIFY_PORT:-5001}" \ --workers ${SERVER_WORKER_AMOUNT:-1} \Show 2 more locations
def post(self, server_code: str): """Handle MCP requests for a specific server.response = AppGenerateService.generate( session=session, app_model=app, user=end_user, args=args, invoke_from=InvokeFrom.SERVICE_API, streaming=app.mode == AppMode.AGENT_CHAT, )Recommended fixRequire TLS termination (nginx or equivalent) in front of this process for any non-loopback deployment, and document that NGINX_HTTPS_ENABLED should default to true (or be mandatory) for production templates.
- 9MediumLogging Hygiene (No Secrets/PII/Tokens) & Audit TrailRepository-wide · ATR-C0005api/libs/helper.py's TokenManager.get_token_data() logs the FULL raw token value at WARNING level (enabled by default under LOG_LEVEL=INFO) whenever a lookup misses. TokenManager mints these tokens for password-reset and other auth flows, so a reset-password token lands in application logs verbatim, with no central redaction filter in place.
@classmethod def get_token_data(cls, token: str, token_type: str) -> dict[str, Any] | None: key = cls._get_token_key(token, token_type) token_data_json = redis_client.get(key) if token_data_json is None: logger.warning("%s token %s not found with key %s", token_type, token, key) return None return dict(_token_data_adapter.validate_json(token_data_json))Show 2 more locations
token = TokenManager.generate_token( account_id=account_id, email=email,LOG_LEVEL: str = Field( description="Logging level, default to INFO. Set to ERROR for production environments.", default="INFO", )Recommended fixChange helper.py:549 to log only a hash/prefix of the token (e.g. token[:8]+'...') or omit it entirely and log the Redis key alone; add a logging filter/formatter that redacts known credential-shaped fields project-wide as defense in depth.
- 10MediumContainer/Runtime HardeningRepository-wide · ATR-C0006Trivy config scan identifies concrete hardening gaps: docker/couchbase-server/Dockerfile uses the floating ':latest' tag (DS-0001) and never sets a non-root USER (DS-0002, HIGH) -- confirmed by manual read, no USER directive exists so the container runs as root by inheritance from the base image. .devcontainer/Dockerfile also runs as root (DS-0002) and misses --no-install-recommends on apt-get install (DS-0029, HIGH). All Dockerfiles lack HEALTHCHECK (DS-0026, LOW). The two internet-facing production images (api/Dockerfile, web/Dockerfile) DO create and switch to a non-root 'dify' user, so the primary attack surface is hardened; the finding is scoped to the auxiliary Couchbase and dev-container images.
FROM couchbase/server:latest AS stage_base # FROM couchbase:latest AS stage_base COPY init-cbserver.sh /opt/couchbase/init/ RUN chmod +x /opt/couchbase/init/init-cbserver.sh
Show 2 more locations
FROM mcr.microsoft.com/devcontainers/python:3.13-bookworm RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install libgmp-dev libmpfr-dev libmpc-devUSER dify
+1 more location in the finding JSON
Recommended fixPin docker/couchbase-server/Dockerfile to a specific Couchbase version tag (not :latest) and add a non-root USER switch after setup; add USER directives to .devcontainer/Dockerfile and use --no-install-recommends there; add HEALTHCHECK instructions to all shipped Dockerfiles.
What’s inside
BlueRock found 8 components in this repository. Each is checked against the rules for its type.
MCP servermcp-server/apiapi34Very low trust3 high
Its tools can
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
MCP server rules 3 of 11 flagged
7 rules passed · 1 did not applyShow
PassedNo Authentication or Authorization Checks (General)ATR-M0010Details
Why it got this result
The MCP endpoint requires an exact match against server_code, a CSPRNG-generated secret that functions as a capability credential, before any request is dispatched; the lookup path denies access on failure and provides no fallback. This satisfies the authentication condition required by this rule. Separate concerns -- such as the lack of an OAuth 2.1 flow and server_code's presence in the URL -- remain tracked under their own dedicated rules.
Why it matters
Prevents unauthorized tool use and data access.
Evidence
def _get_mcp_server_and_app(self, server_code: str, session: Session) -> tuple[AppMCPServer, App]:
"""Get and validate MCP server and app in one query session"""
mcp_server = session.scalar(select(AppMCPServer).where(AppMCPServer.server_code == server_code).limit(1))
if not mcp_server:
raise MCPRequestError(mcp_types.INVALID_REQUEST, "Server Not Found")
app = session.scalar(select(App).where(App.id == mcp_server.app_id).limit(1))
if not app:
raise MCPRequestError(mcp_types.INVALID_REQUEST, "App Not Found")
return mcp_server, appShow 1 more location
def handle_call_tool(
session: Session,
app: App,
request: mcp_types.ClientRequest,
user_input_form: list[VariableEntity],
end_user: EndUser | None,
protocol_version: str = mcp_types.DEFAULT_NEGOTIATED_VERSION,
) -> mcp_types.CallToolResult:
"""Handle call tool request"""
request_obj = cast(mcp_types.CallToolRequest, request.root)
args = prepare_tool_arguments(app, request_obj.params.arguments or {})
if not end_user:
raise ValueError("End user not found")
response = AppGenerateService.generate(
…PassedTool Definition Integrity & Versioning (ETDI-style)ATR-M0001Details
Why it got this result
The MCP server here exposes exactly one tool per published Dify app: a single tool named after the app, whose definition (description/parameters) is derived server-side from the app's own workflow/input-form configuration (core/mcp/server/streamable_http.py handle_list_tools, build_parameter_schema). Tool definitions are not fetched from an external/untrusted URL, not hot-reloaded from third-party JSON, and are regenerated fresh on every ListToolsRequest directly from the app's own DB-backed configuration (which itself requires console RBAC to edit, see controllers/console/app/mcp_server.py AppMCPServerController with RBACCheck(APP_EDIT)). There is no tool-signature/versioning scheme, but there is also no remote/third-party tool manifest ingestion path for this server to rug-pull -- the only mutation path is the app owner editing their own app via an authenticated console API.
Why it matters
Prevents rug-pull tool poisoning.
Evidence
def handle_list_tools(
app_name: str,
app_mode: str,
user_input_form: list[VariableEntity],
description: str,
parameters_dict: dict[str, str],
protocol_version: str = mcp_types.DEFAULT_NEGOTIATED_VERSION,
) -> mcp_types.ListToolsResult:
"""Handle list tools request"""
parameter_schema = build_parameter_schema(app_mode, user_input_form, parameters_dict)
supports_structured = _supports_structured_output(protocol_version)
tool = mcp_types.Tool(
name=app_name,
title=app_name if supports_structured else None,
…Show 1 more location
@console_account_admission(allowed_roles=_EDIT_ROLES, rbac_checks=[RBACCheck(RBACPermission.APP_EDIT, PlainApp())])
def post(self, request_context: RequestContext, app_id: UUID):
payload = validate_request(MCPServerCreatePayload)
try:
server = application_services().app_mcp_servers.create(
request_context, str(app_id), description=payload.description, parameters=payload.parameters
)PassedUnrestricted Tool Permissions (Overprivileged Tools)ATR-M0002Details
Why it got this result
The single MCP tool exposed per server maps 1:1 to the app it was published from, and its invocation is bounded to exactly that app's own workflow/completion/chat execution via AppGenerateService.generate -- it cannot reach arbitrary filesystem paths, arbitrary processes, or other tenants' apps/data. The tool's capability is scoped by construction to the app's own declared input form and workflow graph (which the app owner authored and controls via console RBAC). It is not a generic 'exec' or broad filesystem/network tool.
Why it matters
Limits blast radius and privilege escalation.
Evidence
def handle_call_tool(
session: Session,
app: App,
request: mcp_types.ClientRequest,
user_input_form: list[VariableEntity],
end_user: EndUser | None,
protocol_version: str = mcp_types.DEFAULT_NEGOTIATED_VERSION,
) -> mcp_types.CallToolResult:
"""Handle call tool request"""
request_obj = cast(mcp_types.CallToolRequest, request.root)
args = prepare_tool_arguments(app, request_obj.params.arguments or {})
if not end_user:
raise ValueError("End user not found")
…PassedBinding to 0.0.0.0 Without Auth/TLSATR-M0004Details
Why it got this result
The API/gunicorn process binds to 0.0.0.0:5001 without TLS, but every request requires an exact match against server_code, a CSPRNG-generated capability secret, before the app is invoked; this satisfies the authentication condition this rule evaluates jointly with TLS. The absence of TLS at this layer remains tracked under the dedicated transport-security rule for this component.
Why it matters
LAN/Internet exposure enables remote exploitation.
Evidence
else
if [[ "${DEBUG}" == "true" ]]; then
export HOST=${DIFY_BIND_ADDRESS:-0.0.0.0}
export PORT=${DIFY_PORT:-5001}
exec python -m app
else
exec gunicorn \
--no-control-socket \
--bind "${DIFY_BIND_ADDRESS:-0.0.0.0}:${DIFY_PORT:-5001}" \
--workers ${SERVER_WORKER_AMOUNT:-1} \
--worker-class ${SERVER_WORKER_CLASS:-geventwebsocket.gunicorn.workers.GeventWebSocketWorker} \
--timeout ${GUNICORN_TIMEOUT:-200} \
app:socketio_app
fi
…Show 2 more locations
@mcp_ns.route("/server/<string:server_code>/mcp")
class MCPAppApi(Resource):
@mcp_ns.expect(mcp_ns.models[MCPRequestPayload.__name__])
@mcp_ns.doc("handle_mcp_request")
@mcp_ns.doc(description="Handle Model Context Protocol (MCP) requests for a specific server")
@mcp_ns.doc(params={"server_code": "Unique identifier for the MCP server"})
def post(self, server_code: str):
args = MCPRequestPayload.model_validate(mcp_ns.payload or {}) response = AppGenerateService.generate(
session=session,
app_model=app,
user=end_user,
args=args,
invoke_from=InvokeFrom.SERVICE_API,
streaming=app.mode == AppMode.AGENT_CHAT,
)PassedCORS Misuse: Wildcard with CredentialsATR-M0008Details
Why it got this result
No CORS policy (wildcard or otherwise) is applied to the mcp blueprint at all -- flask-cors is never invoked for mcp_bp (see extensions/ext_blueprints.py), so there is no Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true combination to misuse. (The absence of CORS entirely is a distinct DNS-rebinding/Origin-validation gap already reported under ATR-M0003; this rule specifically targets the wildcard-with-credentials anti-pattern, which is not present here since no CORS directive of any kind is set.)
Why it matters
Prevents credentialed cross-origin abuse.
Evidence
app.register_blueprint(inner_api_bp)
app.register_blueprint(mcp_bp)PassedTool Name Collision / SpoofingATR-M0006Details
Why it got this result
Each MCP server instance is per-app (keyed by server_code, 1 app <-> 1 server) and exposes exactly one tool named after that app's own name -- there is no multi-tool namespace within a single server where collision/shadowing of a well-known tool name is architecturally possible, and the tool name is the app owner's own chosen app name, not an attacker-controlled value distinct from the server's identity.
Why it matters
Prevents malicious tool shadowing.
Evidence
tool = mcp_types.Tool(
name=app_name,
title=app_name if supports_structured else None,
description=description,
inputSchema=cast(dict[str, Any], parameter_schema),
outputSchema={"type": "object"} if supports_structured else None,
)
return mcp_types.ListToolsResult(tools=[tool])PassedPrompt/Metadata Injection in Tool DescriptionsATR-M0007Details
Why it got this result
The tool description surfaced to MCP clients (mcp_server.description, echoed as both instructions on InitializeResult and the tool's description on ListToolsResult) is free text set by the app's own owner/editor/admin through the authenticated console API (RBACCheck(APP_EDIT) in controllers/console/app/mcp_server.py), not attacker-supplied content injected from an untrusted lower-privilege source. There is no sanitization against control characters/zero-width characters, but the only party who can set this text is already the trusted operator of that MCP server/app -- there is no cross-trust-boundary injection path (a different, lower-privileged user cannot write this field and have it surfaced to a higher-trust context). Hardening gap (missing sanitization) is noted but does not constitute a cross-boundary injection vulnerability given the single-trust-level authorship.
Why it matters
Prevents covert model manipulation.
Evidence
def handle_initialize(description: str, requested_version: str | int) -> mcp_types.InitializeResult:
negotiated_version: str = mcp_types.SERVER_LATEST_PROTOCOL_VERSION
if isinstance(requested_version, str) and requested_version in mcp_types.SERVER_SUPPORTED_PROTOCOL_VERSIONS:
negotiated_version = requested_version
capabilities = mcp_types.ServerCapabilities(
tools=mcp_types.ToolsCapability(listChanged=False),
)
return mcp_types.InitializeResult(
protocolVersion=negotiated_version,
capabilities=capabilities,
serverInfo=mcp_types.Implementation(name="Dify", version=dify_config.project.version),
instructions=description,
)Show 1 more location
@console_account_admission(allowed_roles=_EDIT_ROLES, rbac_checks=[RBACCheck(RBACPermission.APP_EDIT, PlainApp())])
def post(self, request_context: RequestContext, app_id: UUID):
payload = validate_request(MCPServerCreatePayload)Does not applyOAuth Proxy Per-Client Consent (Confused Deputy Defense)ATR-M0011Details
Why it got this result
The Dify MCP SERVER component evaluated here (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) does not act as an OAuth proxy forwarding inbound MCP client authorization to a distinct upstream IdP on that client's behalf -- its inbound auth model is the server_code scheme analyzed under ATR-M0010, with no OAuth/consent flow at all. The OAuth 2.1 client flow that DOES exist in this repo (core/mcp/auth/auth_flow.py) is Dify acting as an MCP CLIENT authenticating itself to third-party MCP servers (core/tools/mcp_tool/provider.py), not an OAuth proxy mediating between external MCP clients and an upstream authorization server on their behalf -- there is no dynamic MCP-client-registration endpoint here that issues a shared upstream client_id to third parties. This rule's confused-deputy shape does not apply to this architecture.
Why it matters
Shared upstream client + dynamic MCP-client registration + no per-client consent enables attacker-registered MCP clients to receive bridge codes carrying victim upstream tokens — the documented MCP Confused Deputy attack.
Evidence
def start_authorization(
server_url: str,
metadata: OAuthMetadata | None,
client_information: OAuthClientInformation,
redirect_url: str,
provider_id: str,
tenant_id: str,
scope: str | None = None,
) -> tuple[str, str]:Show its 7 tools
| Tool | It can |
|---|---|
app_invocation_toolDynamically-named MCP tool (named after the published Dify app) that invokes the underlying Dify app (chat/completion/workflow/agent) with caller-supplied query/inputs via AppGenerateService.generate and returns the generated answer/structured content. Registered per-app via ListToolsRequest/CallToolRequest handlers in core/mcp/server/streamable_http.py. | Create or change dataRun code or commandsReturns untrusted content |
delete | |
get | |
head | |
patch | |
post | |
put |
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Skillskills-plugins/.agents/skills/backend-code-review.agents/skills/backend-code-review100High trustNone flagged
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
Skill rules 0 of 8 flagged
6 rules passed · 2 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
SKILL.md frontmatter declares a narrow purpose (review backend code under api/ only, on explicit request) and the body + referenced rule packs (architecture, db-schema, repositories, sqlalchemy) match that declared purpose exactly -- they are read-only review guidance documents with no bundled scripts, no network calls, and no tool invocations beyond reading files. No behavior is hidden or undeclared.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: backend-code-review description: Use only when the user explicitly requests a review or audit of backend code under `api/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, frontend code, or backend code outside `api/`. --- # Backend Code Review Review the requested scope for concrete, reproducible defects. The nearest `AGENTS.md` owns package facts and commands; this skill owns the review workflow and routes to its bundled rule packs. ## Evidence First 1. Establish the requested review scope and inspect the relevant diff or files. 2. Read the changed lines, their behavior owner, nearby tests, and local docstrings or comments that define contracts. 3. Trace callers, persistence boundaries, authorization, generated schemas, or external I/O only when they decide correctness. …
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
This is a review-only skill: it explicitly scopes to reading/reporting findings and states 'Do not add praise sections, speculative risks, or an unsolicited offer to implement fixes.' There is no instruction anywhere in the manifest or references to skip confirmation, suppress warnings, or bypass any human-in-the-loop gate; it does not even implement changes.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
## Severity And Output - **P0**: security or privacy exposure, data loss, or a production-wide outage. - **P1**: user-visible regression, broken authorization or tenant isolation, invalid public contract, or failed primary workflow. - **P2**: concrete correctness, performance, maintainability, or test defect likely to cause incorrect behavior. - **P3**: minor actionable cleanup; omit unless the user requested a thorough audit. Lead with findings ordered by severity. Include a tight file and line reference, the failing contract or reproduction path, impact, and a concrete fix direction. If there are no findings, say `No issues found.` and state any material verification gap. Do not add praise sections, speculative risks, or an unsolicited offer to implement fixes.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
The skill is restricted by its description to backend code under api/ only, read-only review, and it does not request any filesystem/network/tool scope beyond reading repo files -- scope matches purpose.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
description: Use only when the user explicitly requests a review or audit of backend code under `api/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, frontend code, or backend code outside `api/`.
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
Manual review of SKILL.md and its referenced rule packs found no zero-width characters, HTML comments, RTL/LRO override characters, or 'ignore previous instructions'-style injected directives. Grep for common injection markers across all .agents/skills content returned no hits in this skill.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
(full file reviewed; plain markdown prose, no hidden/zero-width characters or concealed directives found)
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
This skill is shipped in-tree as part of the same git-versioned monorepo as the product it reviews, not as a separately-distributed/pulled package; its integrity is bound to the same commit/provenance as the rest of the reviewed source. There is no separate unsigned package manifest or remote update channel for this skill specifically -- mutation would require a repo commit, which is out of scope for this artifact-level rule (no CI/signing infra evaluated here since the skill ships with the source, not as an installable package).
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: backend-code-review description: Use only when the user explicitly requests a review or audit of backend code under `api/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, frontend code, or backend code outside `api/`. ---
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party, in-tree skill shipped by the Dify maintainers in the Dify repo itself; name 'backend-code-review' is a plain descriptive name with no resemblance to a well-known third-party tool/brand, no publisher claim to impersonate.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: backend-code-review
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
The skill does not read host credentials, environment variables, or conversation/memory context; it only reads project files under api/ for review purposes. Because no credential- or conversation-context harvest step exists, the harvesting/exfiltration behavior this rule evaluates is not present in this component.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
## Evidence First 1. Establish the requested review scope and inspect the relevant diff or files. 2. Read the changed lines, their behavior owner, nearby tests, and local docstrings or comments that define contracts. 3. Trace callers, persistence boundaries, authorization, generated schemas, or external I/O only when they decide correctness. 4. Report only findings tied to an observable failure, violated contract, security boundary, data integrity risk, or demonstrated maintenance problem.
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
The component ships only markdown guidance and local reference packs; its SKILL.md contains no scripts, hooks, entry points, host-configuration directives, executable environment assignment, or persistent-state write. No format-native execution or control-plane primitive exists in this skill.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
--- name: backend-code-review description: Use only when the user explicitly requests a review or audit of backend code under `api/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, frontend code, or backend code outside `api/`. --- # Backend Code Review Review the requested scope for concrete, reproducible defects. The nearest `AGENTS.md` owns package facts and commands; this skill owns the review workflow and routes to its bundled rule packs.
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Skillskills-plugins/.agents/skills/e2e-cucumber-playwright.agents/skills/e2e-cucumber-playwright100High trustNone flagged
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
Skill rules 0 of 8 flagged
6 rules passed · 2 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
Declared purpose (authoring/reviewing Cucumber+Playwright E2E tests under e2e/) matches the full body content and referenced docs (cucumber/playwright best practices). No hidden network calls, credential handling, or undeclared capability beyond documentation/routing guidance.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: e2e-cucumber-playwright description: Use when writing, changing, or reviewing Cucumber and Playwright tests under `e2e/`, including feature files, step definitions, support code, scenario tags, locators, and assertions. Do not use for Vitest, React Testing Library, backend tests, or generic browser automation outside the E2E suite. --- # E2E Cucumber And Playwright `e2e/AGENTS.md` owns the suite architecture, lifecycle, commands, tags, generated-client boundaries, fixtures, and cleanup contracts.
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
No instruction to bypass confirmations, hide failures, or disable validation/guardrails anywhere in the skill manifest itself.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
## Workflow 1. Add E2E coverage only for a critical user journey with a cross-boundary outcome that cheaper owner-level tests do not already prove. 2. Identify the user-visible behavior and its feature owner. Start from real product defaults and actor roles; setup may establish preconditions but must not manufacture the opposite state to make the scenario meaningful. 3. Read the target scenario, matching step definitions, and lifecycle files only when session or shared state matters. 4. Reuse an existing step when wording and behavior match; add one coherent scenario or step when they do not. 5. Keep browser actions and assertions at the public user boundary; keep setup, seed, polling, and cleanup at their package-defined owners. 6. For changes, run the narrowest tagged scenario and package checks documented in `e2e/AGENTS.md`; broaden only for shared hooks, tags, or support changes. For review requests, lead with reproducible correctness failures, flake sources, or demonstrated architecture drift. Report the behavior verified and any external-runtime, browser, or environment gap.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
Scope restricted to e2e/ Cucumber/Playwright test authoring and review; no broader capability requested.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
description: Use when writing, changing, or reviewing Cucumber and Playwright tests under `e2e/`, including feature files, step definitions, support code, scenario tags, locators, and assertions. Do not use for Vitest, React Testing Library, backend tests, or generic browser automation outside the E2E suite.
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
No concealed/injected directives found in SKILL.md or its two references (cucumber/playwright best practices); plain prose with external doc links only.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
(full file reviewed; plain markdown prose, no hidden/zero-width characters or concealed directives found)
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
In-tree skill versioned with the repo; no separate distribution/update channel that could mutate after approval independent of normal code review.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: e2e-cucumber-playwright description: Use when writing, changing, or reviewing Cucumber and Playwright tests under `e2e/`, including feature files, step definitions, support code, scenario tags, locators, and assertions. Do not use for Vitest, React Testing Library, backend tests, or generic browser automation outside the E2E suite. ---
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party, in-tree skill; descriptive name referencing the Cucumber/Playwright frameworks it uses, not impersonating or spoofing the Cucumber/Playwright projects themselves (it is a usage guide, not a package claiming to BE one of those tools).
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: e2e-cucumber-playwright
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
The component performs no credential-store or conversation/memory read, so the harvest half of this rule's required pair is absent; the rule explicitly assigns NOT_APPLICABLE when no such mechanism exists in the architecture.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
## Topic Routing Read only the bundled reference required by the change: - Locator, assertion, isolation, or waiting decisions: [`references/playwright-best-practices.md`][playwright] - Scenario wording, step granularity, expressions, World state, hook visibility, or tag design: [`references/cucumber-best-practices.md`][cucumber] Check current official Playwright or Cucumber documentation before introducing a framework pattern that local code and references do not already establish.
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
The component ships only markdown and a display/default-prompt YAML file, with no bundled script, lifecycle hook, entry code, host configuration write, or exec/endpoint-substitution primitive, so the subject of this rule is absent.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
--- name: e2e-cucumber-playwright description: Use when writing, changing, or reviewing Cucumber and Playwright tests under `e2e/`, including feature files, step definitions, support code, scenario tags, locators, and assertions. Do not use for Vitest, React Testing Library, backend tests, or generic browser automation outside the E2E suite. --- # E2E Cucumber And Playwright `e2e/AGENTS.md` owns the suite architecture, lifecycle, commands, tags, generated-client boundaries, fixtures, and cleanup contracts.
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Skillskills-plugins/.agents/skills/frontend-code-review.agents/skills/frontend-code-review100High trustNone flagged
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
Skill rules 0 of 8 flagged
6 rules passed · 2 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
Declared purpose (review frontend code under web/ or packages/dify-ui/ on explicit request) matches the body and reference routing exactly; no hidden behavior.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: frontend-code-review description: Use only when the user explicitly requests a review or audit of frontend code under `web/` or `packages/dify-ui/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, or backend-only code. --- # Frontend Code Review Review the requested scope for concrete defects and violations of explicit project contracts.
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
Review-only skill with no implementation action and no instruction to suppress findings, warnings, or confirmations.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
## Severity And Output - **P0**: security or privacy leak, data loss, production crash, or inaccessible critical workflow. ... Lead with findings ordered by severity. ... Do not add praise sections, speculative risks, or an unsolicited offer to implement fixes.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
Scope restricted to web/ and packages/dify-ui/ frontend code review only.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
description: Use only when the user explicitly requests a review or audit of frontend code under `web/` or `packages/dify-ui/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, or backend-only code.
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
No concealed/injected directives found.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
(full file reviewed; plain markdown prose, no hidden/zero-width characters or concealed directives found)
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
In-tree skill versioned with the repo; no separate distribution/update channel.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: frontend-code-review description: Use only when the user explicitly requests a review or audit of frontend code under `web/` or `packages/dify-ui/`. Supports pending-change, file-focused, and pasted-diff reviews. Do not use for implementation-only requests, diagnosis without review intent, or backend-only code. ---
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party, in-tree skill with a plain descriptive name; no impersonation.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: frontend-code-review
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
This component is a declarative review-only Markdown bundle with no host-credential or conversation/memory read mechanism; its optional external guidelines link is a documentation fetch, not a data-bearing exfiltration channel. The rule requires an actual harvest mechanism to be present, and none exists here, so the rule does not apply.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
[Web Interface Guidelines]: https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
The component consists solely of SKILL.md and reference Markdown files with no bundled script, hook, entry point, host-config write, or control-plane primitive; absent any format-native execution or persistence mechanism, the rule's applicability condition is not met.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
# Frontend Code Review Review the requested scope for concrete defects and violations of explicit project contracts. This skill owns review decisions; its references route to canonical rules without activating another skill's implementation workflow.
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Skillskills-plugins/.agents/skills/frontend-testing.agents/skills/frontend-testing100High trustNone flagged
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
Skill rules 0 of 8 flagged
6 rules passed · 2 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
Short skill that only routes to web/docs/test.md or packages/dify-ui/docs/testing.md for test-writing guidance; content matches declared scope with no hidden behavior.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: frontend-testing description: Use when writing or changing Vitest or React Testing Library tests under `web/` or `packages/dify-ui/`, or when the user explicitly requests frontend test strategy, including evaluation of an existing strategy. Do not use for frontend code-review-only requests, general testability discussion, Python tests, or Cucumber/Playwright E2E. --- # Frontend Testing Read the testing policy for the package that owns the changed tests: - `web/`: `web/docs/test.md` owns Web test boundaries, environments, and commands. - `packages/dify-ui/`: `packages/dify-ui/docs/testing.md` owns primitive test boundaries, environments, Storybook, and commands.
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
No instructions related to suppressing confirmations or guardrails; it only routes to the owning doc's test policy and explicitly says to report any material verification gap rather than hide it.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
This skill adds no parallel policy or check sequence. Follow the selected owner's requirements, including its validation commands. Recommend deleting low-value tests as readily as adding missing behavior coverage, and report the contract verified and any material verification gap.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
Narrowly scoped to Vitest/RTL test authoring under web/ or packages/dify-ui/.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
description: Use when writing or changing Vitest or React Testing Library tests under `web/` or `packages/dify-ui/`, or when the user explicitly requests frontend test strategy, including evaluation of an existing strategy. Do not use for frontend code-review-only requests, general testability discussion, Python tests, or Cucumber/Playwright E2E.
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
No concealed/injected directives found; short routing skill.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
(full file reviewed; plain markdown prose, no hidden/zero-width characters or concealed directives found)
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
In-tree skill versioned with the repo; no separate distribution/update channel.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: frontend-testing description: Use when writing or changing Vitest or React Testing Library tests under `web/` or `packages/dify-ui/`, or when the user explicitly requests frontend test strategy, including evaluation of an existing strategy. Do not use for frontend code-review-only requests, general testability discussion, Python tests, or Cucumber/Playwright E2E. ---
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party, in-tree skill with a plain descriptive name; no impersonation.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: frontend-testing
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
The skill performs no host-credential or conversation/memory read paired with an outbound channel; it only routes to local testing-policy documents under web/ and packages/dify-ui/. Because the harvest subject this rule evaluates does not exist in this component, NOT_APPLICABLE is the correct classification.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
# Frontend Testing Read the testing policy for the package that owns the changed tests: - `web/`: `web/docs/test.md` owns Web test boundaries, environments, and commands. - `packages/dify-ui/`: `packages/dify-ui/docs/testing.md` owns primitive test boundaries, environments, Storybook, and commands. This skill adds no parallel policy or check sequence. Follow the selected owner's requirements, including its validation commands. Recommend deleting low-value tests as readily as adding missing behavior coverage, and report the contract verified and any material verification gap.
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
The component ships only SKILL.md prose and invokes no bundled script, hook, entry point, host configuration, executable environment assignment, or persistence channel, so the format-native execution primitive this rule evaluates is absent, making NOT_APPLICABLE the correct classification.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
# Frontend Testing Read the testing policy for the package that owns the changed tests: - `web/`: `web/docs/test.md` owns Web test boundaries, environments, and commands. - `packages/dify-ui/`: `packages/dify-ui/docs/testing.md` owns primitive test boundaries, environments, Storybook, and commands. This skill adds no parallel policy or check sequence.
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Skillskills-plugins/.agents/skills/how-to-write-component.agents/skills/how-to-write-component100High trustNone flagged
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
Skill rules 0 of 8 flagged
6 rules passed · 2 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
Routing skill for React/TypeScript component architecture decisions; content matches declared scope exactly, no hidden behavior.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: how-to-write-component description: Use when implementing or refactoring React/TypeScript components and the task requires decisions about component ownership, feature boundaries, state, data flow, effects, or interaction ownership. Do not use for review-only requests, test-only work, copy-only edits, or styling-only changes. --- # How To Write A Component Use this skill to route component architecture decisions to its bundled references. Read only the references required by the current change.
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
Purely an architecture-decision routing skill for implementation guidance; no instruction to bypass verification or consent -- it in fact requires 'Verify the changed behavior and complete the owning package's required checks.'
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
## Scope And Verification Identify the behavior owner, state lifetime, and public contract from the nearby implementation and relevant references. Keep the change within that vertical slice unless the contract requires changes elsewhere. Verify the changed behavior and complete the owning package's required checks.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
Narrowly scoped to React/TypeScript component implementation decisions.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
description: Use when implementing or refactoring React/TypeScript components and the task requires decisions about component ownership, feature boundaries, state, data flow, effects, or interaction ownership. Do not use for review-only requests, test-only work, copy-only edits, or styling-only changes.
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
No concealed/injected directives found.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
(full file reviewed; plain markdown prose, no hidden/zero-width characters or concealed directives found)
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
In-tree skill versioned with the repo; no separate distribution/update channel.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: how-to-write-component description: Use when implementing or refactoring React/TypeScript components and the task requires decisions about component ownership, feature boundaries, state, data flow, effects, or interaction ownership. Do not use for review-only requests, test-only work, copy-only edits, or styling-only changes. ---
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party, in-tree skill with a plain descriptive name; no impersonation.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: how-to-write-component
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
No host-credential or conversation/memory harvest mechanism exists in this component; it only routes to local architecture references, so the credential/context-exfiltration rule does not apply.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
## Topic Routing - Component moves, module boundaries, props, types, or owner placement: read [`references/ownership.md`][ownership]. - Dify UI wrappers or primitive state styling: read the package's [Public API authoring] and [Styling] contracts. - Jotai, form drafts, route identity, URL state, or persistence: read [`references/state.md`][state]. - Generated contracts, nullable API data, Query, mutations, SSR, auth, or workspace state: read [`references/data.md`][data]. - Hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces: read [`references/interactions.md`][interactions] and the overlay guide it references when applicable. Also read [`references/state.md`][state] when the surface owns a draft or other local session state. - Effects, navigation, memoization, preloading, or render cost: read [`references/runtime.md`][runtime].
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
This markdown-only bundle contains no script, lifecycle hook, entry-point code, host configuration, executable environment assignment, or persistent control-plane write, so the format-native execution/persistence rule does not apply.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
# How To Write A Component Use this skill to route component architecture decisions to its bundled references. Read only the references required by the current change.
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Skillskills-plugins/e2e/fixtures/test-materials/e2e-summary-skille2e/fixtures/test-materials/e2e-summary-skill100High trustNone flagged
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
Skill rules 0 of 8 flagged
6 rules passed · 2 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
Trivial fixture skill used by Agent Builder E2E tests; it declares and performs exactly one action (summarize input, include a fixed marker string) with no hidden behavior. This is test fixture content (under e2e/fixtures/test-materials/), not a shipped product skill.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: e2e-summary-skill description: Summarize user input for Agent Builder E2E coverage. --- # e2e-summary-skill Summarize the user input in one concise paragraph. The summary must include E2E_SUMMARY_SKILL_PASS.
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
Trivial test-fixture skill with a single summarization instruction; no consent/guardrail-related directive present.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
# e2e-summary-skill Summarize the user input in one concise paragraph. The summary must include E2E_SUMMARY_SKILL_PASS.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
Declared scope (summarize input for E2E coverage) matches the trivial single-purpose behavior; no over-privilege.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
description: Summarize user input for Agent Builder E2E coverage.
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
The instruction to embed a fixed marker string ('E2E_SUMMARY_SKILL_PASS') is visible plain text, clearly declared as the purpose of this E2E test-fixture skill (verifying that the Agent Builder actually invoked this skill), not a concealed or malicious directive.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
# e2e-summary-skill Summarize the user input in one concise paragraph. The summary must include E2E_SUMMARY_SKILL_PASS.
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
In-tree test-fixture skill versioned with the repo; no separate distribution/update channel.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
--- name: e2e-summary-skill description: Summarize user input for Agent Builder E2E coverage. ---
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
First-party test fixture with a plain descriptive name; no impersonation of any external brand.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
name: e2e-summary-skill
Does not applyHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
The skill reads only the current user input for summarization; it does not read host credentials, prior conversation history, or memory, and defines no outbound channel. The subject matter this rule targets -- a host-credential or conversation-context harvest coupled with an exfiltration path -- is absent from this artifact.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
# e2e-summary-skill Summarize the user input in one concise paragraph. The summary must include E2E_SUMMARY_SKILL_PASS.
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
The component consists solely of a prose-only SKILL.md with no bundled script, hook, entry point, host configuration, persistence write, or execution-enabling environment assignment. The execution- and persistence-related subject matter this rule targets is absent from this artifact.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
# e2e-summary-skill Summarize the user input in one concise paragraph. The summary must include E2E_SUMMARY_SKILL_PASS.
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Skillskills-plugins/skills/difyctlskills/difyctl100High trustNone flagged
What stood out
- Known CVEs in pinned dependency versions, notably chromadb==0.5.20 (CVE-2026-45833 CRITICAL, plus two HIGH CVEs) with no fix currently available, and additional HIGH CVEs in pydantic-ai-slim, golang.org/x/mod, and socket.io-parser (ATR-C0007, HIGH).
- Hardcoded default secrets shipped in docker-compose.yaml/docker-compose-template.yaml (PLUGIN_DAEMON_KEY, PLUGIN_DIFY_INNER_API_KEY, DIFY_AGENT_SERVER_SECRET_KEY, MINIO_SECRET_KEY) take effect on any deployment that does not override them, giving a publicly-known static credential for internal plugin-daemon/inner-API/object-storage channels (ATR-C0002, HIGH).
- Dify's app-as-MCP-server feature (api/controllers/mcp/mcp.py, api/core/mcp/server/streamable_http.py) exposes a remote streamable-HTTP MCP endpoint gated solely by a 16-char CSPRNG 'server_code' embedded in the URL path -- there is no standard Authorization/Bearer check, no OAuth 2.1/PKCE flow, and no rate limiting on the server_code lookup, making it a bearer-URL style credential with brute-force risk and typical URL-secret leakage via proxy/access logs and Referer headers.
- The
mcpFlask blueprint is the only blueprint registered in extensions/ext_blueprints.py with NO CORS/Origin policy applied, leaving no Origin/Host validation in front of a host-side-effectful endpoint that invokes full Dify app generation. - A password-reset / sensitive auth token is logged in full at WARNING level in api/libs/helper.py TokenManager.get_token_data, exposing a usable account-takeover credential to anyone with log access (ATR-C0005, MEDIUM).
- The reference deployment ships insecure-by-default transport settings: the API container's gunicorn process binds 0.0.0.0 with no TLS, and NGINX_HTTPS_ENABLED defaults to false in docker/docker-compose.yaml / docker/.env.example, making TLS opt-in rather than mandatory.
- Insecure deserialization (pickle) used for the RAG embedding cache column; the stored BLOB has no integrity binding, so any future write primitive against that column becomes an RCE vector (ATR-C0009).
- Auxiliary/dev container images (docker/couchbase-server/Dockerfile, .devcontainer/Dockerfile) run as root and/or use a floating ':latest' base tag, unlike the hardened production api/Dockerfile and web/Dockerfile (ATR-C0006, MEDIUM).
- Tool-output-to-LLM-context sanitization (ATR-C0013) could not be fully verified across the plugin-daemon boundary; the agent scratchpad/Observation concatenation path and plugin-daemon tool-result boundary lack confirmed control-character/injection-marker stripping.
- skills/difyctl destructive operations (remove/revoke) have no enforced confirmation prompt, relying on the model volunteering confirmation rather than enforcing it in the agent harness.
Skill rules 0 of 8 flagged
7 rules passed · 1 did not applyShow
PassedCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)ATR-P0001Details
Why it got this result
difyctl SKILL.md declares its purpose (discover/run Dify server operations via CLI) and documents its full behavior including auth, destructive-op handling, and sub-skill installation; no hidden or undeclared capability is present in the manifest text itself.
Why it matters
Install-time trust is granted from the artifact's self-description, so any bundled capability the description does not disclose -- network egress, credential reads, or subprocess execution -- runs as an unconsented backdoor with the host agent's full privileges.
Evidence
--- name: difyctl description: 'Base skill for the difyctl CLI: find any command or Dify operation through help and run it as a command. Use when the task involves difyctl or a Dify server from the command line.' --- # difyctl difyctl talks to a Dify server. Business commands are not built in: the server publishes a catalog of operations, and each one is a command you discover at run time. Always pass `--json`. Help and errors are text on a terminal and JSON in a pipe; `--json` (or `DIFY_OUTPUT=json`) forces JSON. Results and streams are always JSON.
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details
Why it got this result
Before any remove/revoke operation, the skill text imperatively requires confirming with the person first; this is an explicit user-confirmation requirement, not an instruction that suppresses or bypasses consent.
Why it matters
Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.
Evidence
## Destructive operations Operations that remove or revoke have no confirmation prompt. Confirm with the person first.
PassedHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details
Why it got this result
difyctl stores its auth token via OS keyring by default (or a local file with --no-keyring, an explicit opt-in documented to the user), and sends it only to the Dify server the user explicitly configured via --server; there is no instruction to transmit the token or conversation context to any third party or undisclosed destination.
Why it matters
A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.
Evidence
`difyctl login --server https://...` (device flow; `--no-browser` prints the URL and code; `--no-keyring` keeps the token in a file; `--insecure` skips TLS verification). One login at a time. `difyctl use workspace <id>` pins a workspace; `difyctl get workspace` shows them. Scripts skip login with `DIFY_SERVER` and `DIFY_TOKEN`; `DIFY_WORKSPACE_ID` overrides the pin; `DIFY_CONFIG_DIR` and `DIFY_CACHE_DIR` move the files.
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details
Why it got this result
difyctl's declared scope is broad (any Dify server operation the server's own catalog exposes) but that breadth is the explicit, disclosed purpose of the tool -- a generic CLI client to the Dify server's own operation catalog -- not an undisclosed over-grant beyond its stated purpose. The server-side RBAC of the Dify API (console/service-api auth) bounds what operations are actually available to the authenticated user, and difyctl itself requests no filesystem/OS capability beyond reading local config/cache dirs and files the user explicitly passes.
Why it matters
An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.
Evidence
description: 'Base skill for the difyctl CLI: find any command or Dify operation through help and run it as a command. Use when the task involves difyctl or a Dify server from the command line.'
PassedInjection and Concealed Directives in Skill/Plugin Manifest and Instruction TextATR-P0006Details
Why it got this result
No concealed/injected directives found; full manifest reviewed as plain prose documenting CLI usage.
Why it matters
A skill or plugin's always-loaded manifest and instruction carriers (frontmatter/body, `description_for_model`, Gemini `FunctionDeclaration.description` / Vertex AI extension `manifest` / ADK agent `instruction`, tool docstrings, JSON-Schema descriptions) are concatenated into the host agent prompt at operator privilege, so a directive that is hidden with non-printing codepoints, disguised with a homoglyph, forged with role-control tokens, or masked from the rendered view silently commandeers model behavior toward attacker-chosen actions across every tool the artifact serves -- with no code execution required and nothing visible to a reviewer reading the rendered text.
Evidence
(full file reviewed; plain markdown prose, no hidden/zero-width characters or concealed directives found)
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details
Why it got this result
Repository documentation establishes that default skill installation is pinned to the commit the difyctl binary was built from, and refresh of installed skills occurs only through an explicit user-invoked rerun after upgrading difyctl; this is neither silent post-approval mutation nor an unpinned runtime fetch.
Why it matters
A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.
Evidence
## Other skills Scenario skills add guidance for one kind of task. `difyctl get skills` shows them; `difyctl install skills <skills root> --skill <name>` installs one, where `<skills root>` is the folder above this file's folder. Re-running `difyctl install skills <skills root>` after upgrading difyctl refreshes every skill.
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details
Why it got this result
difyctl is the first-party CLI for the Dify project itself, shipped in the Dify repo; its name accurately identifies its own first-party product, not an impersonation of a distinct third party.
Why it matters
Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.
Evidence
--- name: difyctl description: 'Base skill for the difyctl CLI: find any command or Dify operation through help and run it as a command. Use when the task involves difyctl or a Dify server from the command line.' ---
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details
Why it got this result
This component ships only SKILL.md prose describing ordinary model-directed CLI invocation; it contains no script, lifecycle hook, entry point, host config, dotenv, git hook, or exec-enabling/endpoint-substituting assignment that would bring it into this rule's scope.
Why it matters
Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.
Evidence
`difyctl login --server https://...` (device flow; `--no-browser` prints the URL and code; `--no-keyring` keeps the token in a file; `--insecure` skips TLS verification). ... `DIFY_CONFIG_DIR` and `DIFY_CACHE_DIR` move the files. `login` blocks until the browser approval arrives. Run `difyctl login --server <url> --no-browser` as a background job, relay the `open <url>` and `code <code>` lines from its stderr to the user, and do not cancel the job.
Before you put it to work
- Upgrade or disable the chromadb vector-store provider pending a fix for CVE-2026-45833/45830/45831; upgrade pydantic-ai-slim, golang.org/x/mod, and socket.io-parser to patched versions.
- Remove hardcoded fallback secret values from docker-compose*.yaml; fail-fast or auto-generate random secrets on first run instead.
- Add OAuth 2.1 (with PKCE) as the primary authorization mechanism for the inbound MCP server endpoint, or at minimum require a standard Authorization header bearer token in addition to server_code, plus rate limiting/lockout on failed server_code lookups.
- Apply an explicit CORS/Origin allowlist (or host-based validation) to the
mcpFlask blueprint consistent with the other blueprints in extensions/ext_blueprints.py. - Stop logging raw token values in api/libs/helper.py TokenManager.get_token_data; log only a hash/prefix or the Redis key.
- Make TLS mandatory (NGINX_HTTPS_ENABLED default true) for any deployment exposing MCP/API endpoints beyond localhost, and default DIFY_BIND_ADDRESS to loopback for standalone (non-compose) runs.
- Replace pickle-based embedding serialization with a safe format (numpy tobytes/JSON) as defense-in-depth against any future write-path compromise.
- Add non-root USER directives and pin a specific (non-'latest') base image tag for docker/couchbase-server/Dockerfile and .devcontainer/Dockerfile; add HEALTHCHECK to all shipped Dockerfiles.
- Conduct a focused audit of the tool-output-to-context concatenation path (agent scratchpad Observation field) and the plugin-daemon boundary for control-character/prompt-injection-marker sanitization.
- Harden skills/difyctl's destructive-operation guidance into an enforced confirmation gate in the surrounding agent harness.
Repository-wide rules
Checked once for the whole repository: dependencies, secrets, inputs, networking, CI and more. 7 of 15 flagged.
8 rules passedShow
PassedRuntime Command Injection via Process ExecutionATR-C0003Details
Why it got this result
The dify-agent / dify-agent-runtime component's declared purpose is to run arbitrary user-submitted shell scripts/commands as a sandboxed agent tool (shellctl), so argv-based exec is within its declared contract rather than an injection bug. Tracing the sinks: (1) dify-agent-runtime/cmd/runner/main.go uses exec.Command(self, childArgs...) / syscall.Exec(binary, argv, ...) with a fixed, hardcoded self-reexec argv (binary + '--exec' + script_path + cwd) -- the actual user-supplied SCRIPT CONTENT is never concatenated into a shell string for this sink; it is written as file content at scriptPath and only the fixed path string is passed as an argv element, with shebang/sh <path> dispatch (buildExecArgv) -- the semgrep LOW-confidence 'non-static command' flags are the exec of the controller's OWN re-exec'd binary, not attacker-constructed shell text. (2) dify-agent-runtime/internal/server/tmux.go wraps user script execution via tmux using shellJoin/shellQuote helpers that quote each argument before composing the wrapper script, and the user's script itself runs inside a Landlock-restricted (default SHELLCTL_ENABLE_PATH_ISOLATION=true, envvar.go:34-40) sandboxed process tree that confines filesystem access -- i.e. arbitrary command execution is the TOOL'S DECLARED JOB, with sandboxing as the mitigating control, not a hidden injection flaw. api/libs/helper.py:127-128 run(script) (subprocess.getstatusoutput('source /root/.bashrc && '+script), a real shell-injection-shaped sink) has NO callers anywhere in the api/ runtime tree (grep confirms only this definition, zero call sites) -- dead code, not reachable from any HTTP/tool-call path, so it does not establish execution_reachability for a VULNERABLE verdict.
Why it matters
Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.
Evidence
self, _ := os.Executable()
childArgs := []string{"--exec", scriptPath, cwd}
if enableIsolation {
childArgs = append([]string{"--exec", "--landlock"}, childArgs[1:]...)
}
cmd := exec.Command(self, childArgs...)
cmd.Env = env
cmd.Dir = cwdShow 2 more locations
// PathIsolationEnabled returns whether Landlock filesystem isolation is active.
func PathIsolationEnabled() bool {
v, ok := os.LookupEnv(EnvEnablePathIsolation)
if !ok {
return true
}
return v == "true"
}def run(script):
return subprocess.getstatusoutput("source /root/.bashrc && " + script)+3 more locations in the finding JSON
PassedWeaponization: Malicious-Purpose Artifact (Offensive Capability by Design)ATR-R0001Details
Why it got this result
Investigated all 3 YARA matches flagged repo-wide. (1) api/configs/remote_settings_sources/apollo/utils.py:46-47 'Zeus_Malware_Reverse_Shell' -- this is a UDP connect()-to-8.8.8.8 trick used purely to discover the machine's outbound-facing local IP address for Apollo remote-config client registration; there is no socket-to-shell wiring, no subprocess/exec, no network listener -- a benign, extremely common Python idiom misidentified by the socket+connect co-occurrence heuristic. (2) dify-agent/tests/integration/dify_agent/storage/test_terminal_finalization.py:57-61 -- a pytest fixture that spawns a throwaway, loopback-bound (127.0.0.1) redis-server subprocess for an integration test and tears it down in a finally block; it is test infrastructure, not an operational reverse shell (no socket piped to a shell, no attacker-facing listener). (3) e2e/AGENTS.md:26-56 'tool_poisoning secondary-objective wide-span' match itself carries confidence 25 and is explicitly annotated by the scanner as 'likely ordinary prose, not a poisoned tool description' -- reading the cited span confirms it is contributor-facing documentation about E2E test runner architecture (tags, seeds, cleanup) with zero instructions directing model/agent behavior, no injected directives, and no secondary objective. Dify's overall product is a LLM app-development/agent-orchestration platform (not an offensive-security tool), and none of its shipped code, skills, or agent instructions direct generation of malware/exploits/credential theft or mass-abuse tooling.
Why it matters
An artifact whose purpose is to weaponize the agent -- to produce malware, steal credentials, evade defenses, or run attacks at scale -- is dangerous regardless of how securely it is built; posture controls (auth, input validation, least privilege) cannot make a weapon safe, which is why a confirmed weaponization finding caps the trust verdict to `blocked` rather than being averaged against a good posture score. Catching malicious PURPOSE is a distinct safety control, and it must be conservative because offensive capability is dual-use: legitimate pentest, CTF, defensive, and research work must abstain (UNKNOWN), never block -- unless the artifact itself hides from the people who install, run or audit it.
Evidence
def init_ip() -> str:
ip = ""
s = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 53))
ip = s.getsockname()[0]
finally:
if s:
s.close()
return ipShow 2 more locations
@pytest.fixture
def redis_url() -> Iterator[str]:
"""Start an isolated Redis when the binary is available locally."""
redis_server = shutil.which("redis-server")
if redis_server is None:
pytest.skip("redis-server is required for run terminal integration tests")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
process = subprocess.Popen( # noqa: S603
[redis_server, "--bind", "127.0.0.1", "--port", str(port), "--save", "", "--appendonly", "no"],
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT,
)- Generate disposable resource names through `support/naming.ts` with an `E2E` prefix. - Keep deterministic upload material in `fixtures/test-materials/`... - Seed scripts own shared long-lived fixtures; scenarios own disposable resources... - Use typed `DifyWorld` cleanup fields... Registered callbacks run LIFO after typed cleanup queues. - Remove child and referencing resources before owners. Attach cleanup failures to the report instead of swallowing them. (contributor-facing E2E test architecture doc; no agent-directed secondary objective)
PassedMissing or Inadequate Input ValidationATR-C0008Details
Why it got this result
Dify's console/service/web API controllers consistently validate request bodies against typed schemas before use: console endpoints use flask-restx @console_ns.expect(...) model bindings plus a model_validate(SomeModel) decorator pattern (e.g. api/controllers/console/auth/login.py @console_ns.expect(console_ns.models[EmailPayload.__name__]) + @model_validate(EmailPayload)) backed by Pydantic models, and service-api / inner-api endpoints use Pydantic TypeAdapter/BaseModel.model_validate_json for structured payload validation (e.g. api/services/data_source/notion_oauth_gateway.py uses _NOTION_SOURCE_INFO_ADAPTER/_NOTION_PAGE_SUMMARY_ADAPTER TypeAdapters to validate even third-party OAuth provider responses before use). File-upload input is explicitly constrained (extension blacklist, size limits, filename character blacklist) in FileService before any filesystem write. This is the rule's documented negative-indicator pattern (strict schema validation; type/length checks) applied broadly across the controller surface.
Why it matters
Frontline control to prevent multiple classes of vulns.
Evidence
extension = os.path.splitext(filename)[1].lstrip(".").lower()
if any(c in filename for c in ["/", "\\"]):
raise ValueError("Filename contains invalid characters")
if len(filename) > 200:
filename = filename.split(".")[0][:200] + "." + extension
if extension and extension in dify_config.UPLOAD_FILE_EXTENSION_BLACKLIST:
raise BlockedFileExtensionError(f"File extension '.{extension}' is not allowed for security reasons")
if source == "datasets" and extension not in DOCUMENT_EXTENSIONS:
raise UnsupportedFileTypeError()Show 1 more location
class _NotionSourceInfo(TypedDict):
workspace_name: str | None
workspace_icon: str | None
workspace_id: str | None
pages: list[_NotionPageSummary]
total: int
_NOTION_SOURCE_INFO_ADAPTER = TypeAdapter(_NotionSourceInfo)
_NOTION_PAGE_SUMMARY_ADAPTER = TypeAdapter(_NotionPageSummary)PassedInsecure DeserializationATR-C0009Details
Why it got this result
pickle is used to serialize/deserialize RAG embedding vectors (api/models/dataset.py:744-747, api/core/rag/embedding/cached_embedding.py:115,198), but tracing the write path shows embedding bytes are produced exclusively by CacheEmbedding, which pickles the FLOAT LIST returned directly from the configured embedding model's own invoke_text_embedding()/invoke_multimodal_embedding() output -- there is no code path where externally-supplied bytes (an uploaded file, an HTTP body, a tool argument) are passed to pickle.loads. Reaching the sink with attacker-chosen bytes would require an independent primitive (e.g. direct DB write access or a different injection bug) that is not present in this codebase, so the required attacker_controlled_source and continuous source_to_sink_flow this rule demands cannot be established from the cited code; the evaluation_rubric's 'Deserialization of constants only' false_positive_guard class applies by analogy (model-generated, not request-controlled). Separately, api/services/agent/roster_package_reader.py:352-379 deserializes untrusted Roster Agent package YAML using a hardened yaml.SafeLoader subclass (_PackageYamlLoader) that explicitly rejects YAML aliases and non-string mapping keys -- the one YAML-deserialization sink that DOES take externally-supplied (uploaded package) content uses the safe loader, which is the rule's documented negative indicator.
Why it matters
Can lead to arbitrary code execution.
Evidence
cache_embeddings = []
try:
for i, n_embedding in zip(embedding_queue_indices, embedding_queue_embeddings):
text_embeddings[i] = n_embedding
hash = helper.generate_text_hash(texts[i])
if hash not in cache_embeddings:
embedding_cache = Embedding(
model_name=self._model_instance.model_name,
hash=hash,
provider_name=self._model_instance.provider,
embedding=pickle.dumps(n_embedding, protocol=pickle.HIGHEST_PROTOCOL),
)Show 2 more locations
def set_embedding(self, embedding_data: list[float]):
self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL)
def get_embedding(self) -> list[float]:
return cast(list[float], pickle.loads(self.embedding)) # noqa: S301 return yaml.load(payload, Loader=_PackageYamlLoader), size # noqa: S506
except (UnicodeDecodeError, yaml.YAMLError, ValueError, RecursionError) as exc:
raise InvalidRosterAgentPackageError(...)
f"Roster Agent package {path.removesuffix('.yaml')} is invalid"
) from exc
class _PackageYamlLoader(yaml.SafeLoader):
"""Reject ambiguous mappings and aliases in untrusted package documents."""
@overridePassedCI/CD Workflow Script InjectionATR-C0014Details
Why it got this result
Reviewed all workflows using dangerous triggers (workflow_run, repository_dispatch, workflow_dispatch) for attacker-controlled interpolation into shell run blocks. The i18n translation pipeline (trigger-i18n-sync.yml -> translate-i18n-claude.yml) passes attacker-influenceable data (PR/commit content) only through typed client_payload fields consumed as quoted shell variables (BASE_SHA="..."), and the PR-coverage-comment workflows (pyrefly-diff-comment.yml, pyrefly-type-coverage-comment.yml) using workflow_run only check out the DEFAULT branch (trusted code) and fetch data via the GitHub API/artifacts rather than interpolating untrusted strings directly into a run: block. The 12 semgrep yaml.github-actions.security.run-shell-injection hits (accessibility-e2e.yml, cli-e2e.yml, pyrefly-diff.yml, pyrefly-type-coverage.yml, style.yml, translate-i18n-claude.yml) all resolve to either workflow_dispatch 'choice' inputs enforced by a fixed options list, maintainer-only manual dispatch, values interpolated only within an already-double-quoted shell string consumed as data (not re-parsed/eval'd), or SHA values resolved via git/GitHub Actions context rather than free-form attacker text. No case shows a continuous source-to-sink path from an untrusted field to an unguarded shell execution with secrets/write-token exposure.
Why it matters
CI/CD script injection can execute attacker-controlled commands with workflow tokens, repository secrets, trusted-runner access, artifact or cache authority, and release or deployment privileges.
Evidence
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
BASE_SHA="${{ github.event.client_payload.base_sha }}"
HEAD_SHA="${{ github.event.client_payload.head_sha }}"
CHANGED_FILES="${{ github.event.client_payload.changed_files }}"
...
if [ -n "${{ github.event.client_payload.changes_base64 }}" ]; then
printf '%s' '${{ github.event.client_payload.changes_base64 }}' | base64 -d > /tmp/i18n-changes.jsonShow 1 more location
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
steps:
- name: Checkout default branch (trusted code)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1PassedDoS Protections: Timeouts, Concurrency, Streaming LimitsATR-C0004Details
Why it got this result
The codebase implements layered DoS protections: (1) a generic rate-limit decorator/library (api/libs/rate_limit.py) with per-IP/session/account/email/token sliding-window limits applied to auth-sensitive endpoints (device code, SSO initiate, bearer token validation, etc.); (2) the outbound SSRF-protected HTTP client enforces connect/read/write/total timeouts by default (SSRF_DEFAULT_TIME_OUT family) and a bounded buffer_response() that raises ResponseTooLargeError once decoded bytes exceed a configured cap, preventing unbounded streaming responses from exhausting memory; (3) upload size limits are enforced per content type (UPLOAD_FILE_SIZE_LIMIT, UPLOAD_IMAGE_FILE_SIZE_LIMIT, UPLOAD_VIDEO_FILE_SIZE_LIMIT, UPLOAD_AUDIO_FILE_SIZE_LIMIT, UPLOAD_SKILL_FILE_SIZE_LIMIT) in api/configs/feature/__init__.py and enforced in FileService.is_file_size_within_limit before any save() call.
Why it matters
Resource exhaustion can crash or stall MCP.
Evidence
def buffer_response(response: httpx.Response, *, max_response_bytes: int) -> httpx.Response:
"""Consume one open identity response under a decoded byte limit and close its stream."""
if max_response_bytes <= 0:
raise ValueError("max_response_bytes must be positive")
try:
content_encoding = response.headers.get("content-encoding", "identity").strip().lower()
if content_encoding not in {"", "identity"}:
raise UnsupportedResponseEncodingError(...)
content = bytearray()
for chunk in response.iter_bytes():
if len(content) + len(chunk) > max_response_bytes:
raise ResponseTooLargeError(f"response exceeded {max_response_bytes} bytes")
content.extend(chunk)Show 2 more locations
LIMIT_DEVICE_CODE_PER_IP = RateLimit(60, timedelta(hours=1), (RateLimitScope.IP,)) LIMIT_SSO_INITIATE_PER_IP = RateLimit(60, timedelta(hours=1), (RateLimitScope.IP,)) LIMIT_APPROVE_EXT_PER_EMAIL = RateLimit(10, timedelta(hours=1), (RateLimitScope.SUBJECT_EMAIL,)) LIMIT_DEVICE_FLOW_APPROVE = RateLimit(...) LIMIT_LOOKUP_PUBLIC = RateLimit(60, timedelta(minutes=5), (RateLimitScope.IP,)) LIMIT_BEARER_PER_TOKEN = RateLimit(...)
UPLOAD_FILE_SIZE_LIMIT: NonNegativeInt = Field(...)
KNOWLEDGE_UPLOAD_FILE_SIZE_LIMIT_FOR_PAID_PLAN: NonNegativeInt = Field(...)
UPLOAD_IMAGE_FILE_SIZE_LIMIT: NonNegativeInt = Field(...)
UPLOAD_VIDEO_FILE_SIZE_LIMIT: NonNegativeInt = Field(...)
UPLOAD_AUDIO_FILE_SIZE_LIMIT: NonNegativeInt = Field(...)
UPLOAD_SKILL_FILE_SIZE_LIMIT: NonNegativeInt = Field(...)PassedLack of Output Encoding/SanitizationATR-C0010Details
Why it got this result
The semgrep XSS-shaped hits (python.flask.security.audit.xss.make-response-with-unknown-content, 8 occurrences across login.py, agent_config_inspector.py, recommended_app.py, tool_providers.py, trigger_providers.py) all resolve on inspection to make_response(dump_response(SomeTypedModel, {...})) calls -- i.e. a structured, schema-typed JSON-shaped payload serialized through a typed response-dump helper, not raw/unescaped user input written into an HTML content-type response. The project's web frontend (React/Next.js in web/) uses JSX's default auto-escaping for rendered text and the two render-template-string hits (api/libs/email_template_renderer.py, api/tasks/mail_inner_task.py) go through a Jinja2 ImmutableSandboxedEnvironment in SANDBOX mode by default (MAIL_TEMPLATING_MODE default = SANDBOX), which blocks template-level code/attribute-traversal exploitation even if the template string itself were attacker-influenced.
Why it matters
Prevents UI-level injection and operator compromise.
Evidence
response = make_response(dump_response(SimpleResultResponse, {"result": "success"}))
clear_access_token_from_cookie(response)
clear_refresh_token_from_cookie(response)
clear_csrf_token_from_cookie(response)
return responseShow 2 more locations
class SandboxedEnvironment(ImmutableSandboxedEnvironment):
"""Sandboxed environment with execution timeout."""
def __init__(self, timeout: int, *args: Any, **kwargs: Any):
self._deadline = time.time() + timeout if timeout else None
super().__init__(*args, **kwargs)
@override
def call(self, context: Context, obj: Any, *args: Any, **kwargs: Any) -> Any:
if self._deadline is not None and time.time() > self._deadline:
raise TimeoutError("Template rendering timeout")
return super().call(context, obj, *args, **kwargs) MAIL_TEMPLATING_MODE: TemplateMode = Field(
description="Template mode for email services",
default=TemplateMode.SANDBOX,
)PassedDenial of Wallet / Cost ControlsATR-C0012Details
Why it got this result
Model/tool invocation paths are protected by multiple quota/budget layers: api/libs/rate_limit.py provides per-IP/account/session/email/token sliding-window rate limits applied to auth and API-bearer-token validation; FeatureService (api/services/feature_service.py referenced throughout controllers, e.g. human_input_delivery_test_service.py's features.human_input_email_delivery_enabled gate) enforces plan-based feature/quota gating before billable operations execute; and dataset/knowledge upload size limits (UPLOAD_FILE_SIZE_LIMIT family) bound the cost of ingestion-triggered embedding calls. The SSRF-protected HTTP client additionally bounds retries (SSRF_DEFAULT_MAX_RETRIES) and response size (buffer_response max_response_bytes), preventing unbounded retry storms or oversized-response costs on outbound calls to paid third-party APIs/model providers.
Why it matters
Prevents cost-based disruption.
Evidence
LIMIT_DEVICE_CODE_PER_IP = RateLimit(60, timedelta(hours=1), (RateLimitScope.IP,)) LIMIT_SSO_INITIATE_PER_IP = RateLimit(60, timedelta(hours=1), (RateLimitScope.IP,)) LIMIT_APPROVE_EXT_PER_EMAIL = RateLimit(10, timedelta(hours=1), (RateLimitScope.SUBJECT_EMAIL,)) LIMIT_BEARER_PER_TOKEN = RateLimit(...)
Show 1 more location
retries = 0
while retries <= max_retries:
...
retries += 1
if retries <= max_retries:
time.sleep(BACKOFF_FACTOR * (2 ** (retries - 1)))
raise MaxRetriesExceededError(f"Reached maximum retries ({max_retries}) for URL {url}")You know what it is. Now see what it does.
A scan helps you understand what you’re building with. When your agent runs, BlueRock shows you what actually happens across tools and systems. Follow actions in context, understand behavior as it unfolds and add controls when you need them.
Results reflect the code at the commit and date shown.