Back to skill

Security audit

Claw Stack Manager

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it claims at a high level, but it has under-disclosed, high-impact Docker and credential-handling behavior that users should review carefully before installing.

Install only if you are comfortable giving this skill strong Portainer control over the configured Docker endpoint. Before use, remove or review the hard-coded .env loading, rotate any Portainer key previously used with this implementation, avoid running update until the helper-container key exposure and ng-agent deletion behavior are fixed, and use a narrowly scoped Portainer API key where possible.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (5)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/manage.py:12
Finding
Unrestricted Loading of a Hard-Coded Workspace Credential File<![CDATA[ ## Vulnerability Details **File Location**: `scripts/manage.py`, lines 12–19 **Vulnerability Type**: Unauthorized access to credential-bearing files **Risk Level**: High ### Vulnerable Code ```python # Auto-load .env from workspace root _env_path = '/home/node/.openclaw/workspace/liyj/.env' if os.path.isfile(_env_path): with open(_env_path) as _f: for _line in _f: _line = _line.strip() if _line and '=' in _line and not _line.startswith('#'): _k, _v = _line.split('=', 1) os.environ.setdefault(_k.strip(), _v.strip()) ``` ### Technical Analysis The Skill silently reads every key-value pair from a hard-coded `.env` file belonging to a specific workspace. Its documented functionality only requires a small set of Portainer-related settings, but the implementation imports all entries into the process environment without filtering. This exceeds least privilege because unrelated credentials may be present in the same file. The behavior is neither required by the declared stack-management functionality nor disclosed in `SKILL.md`. The hard-coded user-specific path also makes the Skill non-portable and risks crossing workspace or tenant boundaries when deployed in a shared environment. Although the current script only explicitly uses selected environment variables, importing unrelated secrets unnecessarily exposes them to the process and to any future code executed in that process. ### Attack Path 1. A user invokes the Skill on a host where `/home/node/.openclaw/workspace/liyj/.env` exists. 2. The Skill opens the file without explicit user authorization or an opt-in command-line option. 3. Every syntactically valid entry is imported into `os.environ`. 4. Unrelated credentials become accessible to the Skill process and any subsequently introduced or invoked code. 5. A separate flaw, malicious modification, or diagnostic behavior could then disclose or misuse those credentials. ### Impact ...[truncated 322 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the hard-coded automatic `.env` loading behavior. - Require callers to provide only the documented variables through the execution environment. - If file-based configuration is necessary, add an explicit `--env-file` option and require informed user consent. - Allowlist only `PORTAINER_API_KEY`, `PORTAINER_URL`, `PORTAINER_ENDPOINT`, `CLAW_STACK_ID`, and `CLAW_IMAGE`. - Reject unknown keys instead of importing the entire file. - Verify that any configuration file is owned by the expected user and is not group- or world-readable. - Avoid binding the Skill to a user-specific absolute path. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/manage.py:134
Finding
Portainer API Key Persisted in Docker Container Command Metadata<![CDATA[ ## Vulnerability Details **File Location**: `scripts/manage.py`, lines 134–174 **Vulnerability Type**: Plaintext credential exposure **Risk Level**: Critical ### Vulnerable Code ```python if need_stop: script = ( 'apk add -q curl\n' f'echo "R: 1/2 Stopping stack {stack_id}..."\n' f'curl -sS -X POST -H "X-API-Key: {KEY}" ' f'{HOST}/api/stacks/{stack_id}/stop?endpointId={EP} >/dev/null\n' f'echo ""\n' f'echo "R: 2/2 Deploying with updated config..."\n' f'curl -sS -X PUT -H "X-API-Key: {KEY}" ' f'-H "Content-Type: application/json" ' f'-d \'@-\' ' f'{HOST}/api/stacks/{stack_id}?endpointId={EP} << "PAYLOAD"\n' f'{payload}\n' f'PAYLOAD\n' f'echo ""\n' f'echo "R: Done."\n' ) else: script = ( 'apk add -q curl\n' f'echo "R: Stack already stopped, deploying..."\n' f'curl -sS -X PUT -H "X-API-Key: {KEY}" ' f'-H "Content-Type: application/json" ' f'-d \'@-\' ' f'{HOST}/api/stacks/{stack_id}?endpointId={EP} << "PAYLOAD"\n' f'{payload}\n' f'PAYLOAD\n' f'echo ""\n' f'echo "R: Done."\n' ) # Use unique name to avoid collision ts = datetime.now().strftime("%Y%m%d%H%M%S") container_name = f"claw-redep-{ts}" config = { "Image": "alpine:latest", "Cmd": ["/bin/sh", "-c", script.strip()], "HostConfig": {"NetworkMode": "host"}, "Labels": {"io.portainer.stack.name": f"redeployer-{stack_id}"} } ``` ### Technical Analysis The Portainer API key is interpolated directly into a generated shell script. That entire script is then assigned to the Docker container's `Cmd` field. Docker and Portainer retain container configuration metadata, including command arguments. Consequently, the plaintext API key can be recovered through container inspection, Portainer API responses, daemon backups, debugging output, or other management interfaces. The container is no ...[truncated 1108 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Never place API keys in Docker `Cmd`, command-line arguments, labels, or image metadata. - Prefer performing the stop and update requests directly from the Python process instead of creating a helper container. - If a helper container is unavoidable, provide the credential through a protected, short-lived secret file or an appropriate Docker secrets mechanism. - Use a narrowly scoped, short-lived Portainer token rather than a persistent administrative key. - Configure helper containers for automatic removal after completion. - Track and explicitly remove any failed or abandoned helper containers. - Restrict container-inspection permissions and Portainer API access. - Rotate any Portainer key previously used by this implementation because it may already be retained in container metadata. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/manage.py:134
Finding
Shell Command Injection Through Unsafely Interpolated Configuration Values<![CDATA[ ## Vulnerability Details **File Location**: `scripts/manage.py`, lines 134–162 **Vulnerability Type**: Shell command injection **Risk Level**: High ### Vulnerable Code ```python if need_stop: script = ( 'apk add -q curl\n' f'echo "R: 1/2 Stopping stack {stack_id}..."\n' f'curl -sS -X POST -H "X-API-Key: {KEY}" ' f'{HOST}/api/stacks/{stack_id}/stop?endpointId={EP} >/dev/null\n' f'echo ""\n' f'echo "R: 2/2 Deploying with updated config..."\n' f'curl -sS -X PUT -H "X-API-Key: {KEY}" ' f'-H "Content-Type: application/json" ' f'-d \'@-\' ' f'{HOST}/api/stacks/{stack_id}?endpointId={EP} << "PAYLOAD"\n' f'{payload}\n' f'PAYLOAD\n' f'echo ""\n' f'echo "R: Done."\n' ) else: script = ( 'apk add -q curl\n' f'echo "R: Stack already stopped, deploying..."\n' f'curl -sS -X PUT -H "X-API-Key: {KEY}" ' f'-H "Content-Type: application/json" ' f'-d \'@-\' ' f'{HOST}/api/stacks/{stack_id}?endpointId={EP} << "PAYLOAD"\n' f'{payload}\n' f'PAYLOAD\n' f'echo ""\n' f'echo "R: Done."\n' ) ``` The generated value is subsequently executed by a shell: ```python config = { "Image": "alpine:latest", "Cmd": ["/bin/sh", "-c", script.strip()], "HostConfig": {"NetworkMode": "host"}, "Labels": {"io.portainer.stack.name": f"redeployer-{stack_id}"} } ``` ### Technical Analysis `HOST` and `KEY` originate from environment variables and are directly interpolated into a shell program without shell-safe argument separation or validation. The resulting string is executed by `/bin/sh -c`. Shell metacharacters embedded in these values can alter command structure. For example, a malicious `PORTAINER_URL` containing a command separator can append commands to the generated `curl` line. The hard-coded `.env` loader increases the available configuration source through which ...[truncated 1441 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Eliminate dynamically generated shell scripts and perform Portainer requests directly with Python. - If a helper process is necessary, use an argument array rather than `/bin/sh -c`. - Validate `PORTAINER_URL` with a strict URL parser and allow only expected schemes, hosts, and ports. - Never interpolate credentials into shell source. - Pass request data through a protected file descriptor or temporary file created with restrictive permissions. - Treat configuration-file contents as untrusted input even when loaded from a local workspace. - Add tests covering command separators, quotes, substitutions, newlines, and other shell metacharacters. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/manage.py:108
Finding
Undeclared Forced Deletion of Potentially Unrelated Containers<![CDATA[ ## Vulnerability Details **File Location**: `scripts/manage.py`, lines 108–112 and 176–177 **Vulnerability Type**: Excessive privilege and destructive resource management **Risk Level**: High ### Vulnerable Code ```python def cleanup_container(name): """Remove stale containers by name pattern.""" enc = urllib.parse.quote(json.dumps({"name": [name]})) s, cs = api("GET", f"/endpoints/{EP}/docker/containers/json?all=true&filters={enc}") for c in (cs or []): api("DELETE", f"/endpoints/{EP}/docker/containers/{c['Id']}?force=true") ``` The cleanup function is invoked for both redeployer containers and an unrelated name: ```python cleanup_container("claw-redep-") cleanup_container("ng-agent") ``` ### Technical Analysis The cleanup operation searches containers using name filters and force-deletes every result. It does not verify an exact container identity, an ownership label, the creating Skill instance, or whether the container is safe to remove. Deleting containers named `ng-agent` is not part of the functionality documented in `SKILL.md` and is not necessary to update a Portainer stack. The `force=true` option can terminate a running container before deletion, creating an immediate availability impact. The broad `claw-redep-` name filter can also affect similarly named containers not created by the current execution. ### Attack Path 1. A legitimate service or management agent has a container name matching `ng-agent` or the `claw-redep-` filter. 2. A user invokes the Skill in `update` mode. 3. `create_update_redeployer` calls both cleanup operations. 4. The Portainer API returns all matching containers. 5. The Skill issues a force-delete request for every returned container. 6. Matching services are terminated and removed without additional confirmation or ownership validation. ### Impact Assessment The Skill can destroy unrelated Docker containers on the configured endpoint. This may interrupt monitoring, management agen ...[truncated 212 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove `cleanup_container("ng-agent")`. - Track helper containers by exact ID rather than a partial name. - Apply a dedicated ownership label containing the Skill identity and execution identifier. - Before deletion, verify both the exact expected label and container ID. - Avoid `force=true` unless the user explicitly authorizes termination of a known container. - Configure one-shot helper containers for automatic removal where supported. - Report cleanup failures instead of broadening the deletion criteria. - Document every destructive operation in `SKILL.md` and require confirmation for operations outside the selected stack. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/manage.py:134
Finding
Mutable Redeployer Image and Unpinned Runtime Package Installation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/manage.py`, lines 134–170 **Vulnerability Type**: Unsafe and non-reproducible dependency retrieval **Risk Level**: Medium ### Vulnerable Code ```python if need_stop: script = ( 'apk add -q curl\n' f'echo "R: 1/2 Stopping stack {stack_id}..."\n' f'curl -sS -X POST -H "X-API-Key: {KEY}" ' f'{HOST}/api/stacks/{stack_id}/stop?endpointId={EP} >/dev/null\n' f'echo ""\n' f'echo "R: 2/2 Deploying with updated config..."\n' f'curl -sS -X PUT -H "X-API-Key: {KEY}" ' f'-H "Content-Type: application/json" ' f'-d \'@-\' ' f'{HOST}/api/stacks/{stack_id}?endpointId={EP} << "PAYLOAD"\n' f'{payload}\n' f'PAYLOAD\n' f'echo ""\n' f'echo "R: Done."\n' ) else: script = ( 'apk add -q curl\n' f'echo "R: Stack already stopped, deploying..."\n' f'curl -sS -X PUT -H "X-API-Key: {KEY}" ' f'-H "Content-Type: application/json" ' f'-d \'@-\' ' f'{HOST}/api/stacks/{stack_id}?endpointId={EP} << "PAYLOAD"\n' f'{payload}\n' f'PAYLOAD\n' f'echo ""\n' f'echo "R: Done."\n' ) config = { "Image": "alpine:latest", "Cmd": ["/bin/sh", "-c", script.strip()], "HostConfig": {"NetworkMode": "host"}, "Labels": {"io.portainer.stack.name": f"redeployer-{stack_id}"} } ``` ### Technical Analysis The helper container uses the mutable `alpine:latest` image tag and installs `curl` from configured package repositories every time it runs. Neither the base image digest nor the package version is pinned. As a result, the executed dependency set can change after the Skill has been reviewed, without any modification to the Skill package. A compromised registry account, package repository, mirror, or mutable image tag could introduce malicious behavior into a container that receives host-network access and contains a Portainer API key ...[truncated 1020 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace `alpine:latest` with a reviewed image pinned by immutable digest. - Build a minimal redeployer image containing the required HTTP client in advance. - Pin package versions during the image build and retain a lockable, reproducible build process. - Verify image signatures, provenance, and software bills of materials where available. - Use an approved registry and restrict image pulls to trusted repositories. - Scan the resulting image for known vulnerabilities before deployment. - Avoid installing packages dynamically while a sensitive credential is present. - Prefer direct Python API calls, which would remove the need for the helper image and runtime package installation entirely. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (10)

Tainted flow: 'req' from os.environ.get (line 49, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
if data is not None:
        req.data = json.dumps(data).encode()
    try:
        resp = urllib.request.urlopen(req, timeout=timeout)
        if raw:
            return resp.status, resp.read()
        return resp.status, json.loads(resp.read())
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Credential Access

High
Category
Privilege Escalation
Content
import json, os, urllib.parse, urllib.request, sys
from datetime import datetime, timezone, timedelta

# Auto-load .env from workspace root
_env_path = '/home/node/.openclaw/workspace/liyj/.env'
if os.path.isfile(_env_path):
    with open(_env_path) as _f:
Confidence
93% confidence
Finding
The script automatically loads credentials from a hard-coded .env path in the workspace. This creates implicit credential access behavior and couples privileged API use to a developer-specific file location, which can surprise operators and expose secrets if the workspace is shared or improperly protected.

Credential Access

High
Category
Privilege Escalation
Content
from datetime import datetime, timezone, timedelta

# Auto-load .env from workspace root
_env_path = '/home/node/.openclaw/workspace/liyj/.env'
if os.path.isfile(_env_path):
    with open(_env_path) as _f:
        for _line in _f:
Confidence
93% confidence
Finding
The .env parsing logic reads arbitrary key/value pairs from a local workspace file into process environment variables without user confirmation. In this privileged management script, that behavior can silently import sensitive credentials or attacker-controlled configuration that alters where privileged API requests are sent.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The code force-deletes containers matching 'ng-agent' even though that action is unrelated to the stated stack redeploy purpose. Deleting unrelated containers can cause denial of service, interfere with other agents or security tooling, and exceeds the principle of least privilege.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill exposes capabilities that can access environment variables, make network requests, and execute shell commands, but the manifest does not declare any tool scope or permission boundaries. For a stack-management skill that can stop and redeploy services through the Portainer API, this lack of explicit scope increases the chance of unintended or overly broad execution in an automation environment.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The documentation describes update and redeploy behavior that stops stacks and replaces running services, but it does not clearly warn operators about service interruption, replacement risk, or the operational impact of targeting the wrong stack. In this context, that omission is security-relevant because the skill performs high-impact infrastructure actions and could be invoked without adequate operator awareness or safeguards.

External Transmission

Medium
Category
Data Exfiltration
Content
script = (
            'apk add -q curl\n'
            f'echo "R: 1/2 Stopping stack {stack_id}..."\n'
            f'curl -sS -X POST -H "X-API-Key: {KEY}" '
            f'{HOST}/api/stacks/{stack_id}/stop?endpointId={EP} >/dev/null\n'
            f'echo ""\n'
            f'echo "R: 2/2 Deploying with updated config..."\n'
Confidence
90% confidence
Finding
The script embeds the Portainer API key and full stack payload into a shell script executed inside a temporary container over host networking. This transmits privileged credentials and configuration into another execution environment, increasing exposure through process inspection, logs, container metadata, or compromise of that helper container.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The skill claims to manage stacks, but it also directly restarts a specific container outside stack-scoped operations. This creates a broader operational capability than advertised, increasing the chance of unintended service disruption or misuse by callers who expect only stack-level actions.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script performs impactful actions such as image pulls, stack stop/redeploy, and container restart without an interactive confirmation or a dry-run safeguard. In an agent skill context, this makes accidental or unintended destructive operations more likely, especially when invoked programmatically.

Missing User Warnings

Low
Confidence
76% confidence
Finding
The script fetches stack environment metadata from Portainer and logs the variable names. Even without values, variable names can disclose sensitive implementation details such as the presence of secret-bearing configuration, third-party integrations, or internal service names.

Static analysis

No suspicious patterns detected.