Back to skill

Security audit

Portainer

Security checks for vulnerabilities and agentic risk

Overview

This Portainer skill is mostly transparent about managing Docker infrastructure, but it exposes very powerful infrastructure controls with weak safety boundaries and insecure TLS handling.

Install only if you trust the operator context and can limit the Portainer API token to the least privileges needed. Avoid using this against production infrastructure until TLS verification is fixed, raw Docker proxying is restricted or removed, and destructive actions require explicit confirmation.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/portainer_manager.py:11
Finding
Privileged Portainer API Token Transmitted Without TLS Certificate Verification<![CDATA[ ## Vulnerability Details **File Location**: `scripts/portainer_manager.py:11-14, 20-26, 35-41, 51-57, 82-88, 96-103, 114-120, 125-134` **Vulnerability Type**: Sensitive credential exposure through disabled TLS authentication **Risk Level**: High ### Vulnerable Code ```python # Attempt to get Portainer API URL from environment, default to https://localhost:9443/api PORTAINER_API_URL = os.environ.get("PORTAINER_API_URL", "https://localhost:9443/api") # Suppress warnings for self-signed certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def get_portainer_api_token(): token = os.environ.get("PORTAINER_API_TOKEN") if not token: raise ValueError("PORTAINER_API_TOKEN environment variable not set.") return token def list_environments(): print(f"Attempting to list Portainer environments via {PORTAINER_API_URL}...", flush=True) try: token = get_portainer_api_token() headers = {"X-API-Key": token, "Content-Type": "application/json"} response = requests.get( f"{PORTAINER_API_URL}/endpoints", headers=headers, timeout=10, verify=False ) ``` The same `verify=False` setting is used by all Portainer requests, including stack inspection, deployment, update, removal, and arbitrary Docker API proxy operations. ### Technical Analysis The Portainer API token is placed in the `X-API-Key` header and transmitted over HTTPS, but every request explicitly disables certificate verification. Consequently, HTTPS encryption is used without authenticating the remote server. Any certificate—including a self-signed certificate controlled by an attacker—is accepted. The application also globally suppresses `InsecureRequestWarning`, preventing operators from receiving the warning normally emitted for this unsafe behavior. Because `PORTAINER_API_URL` is configurable through the environment, an attacker who can alter configuration can redirect t ...[truncated 1995 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `verify=False` from every `requests.get`, `requests.post`, `requests.put`, and `requests.delete` call so that normal certificate validation is enforced. 2. Remove the global suppression of `urllib3.exceptions.InsecureRequestWarning`. 3. For private or self-signed Portainer deployments, support an explicit CA bundle: ```python PORTAINER_CA_BUNDLE = os.environ.get("PORTAINER_CA_BUNDLE", True) response = requests.get( url, headers=headers, timeout=10, verify=PORTAINER_CA_BUNDLE ) ``` 4. Reject non-HTTPS URLs except for an explicitly enabled localhost-only development mode. 5. Validate or allowlist the configured Portainer hostname to reduce credential redirection risk. 6. Use a dedicated, least-privileged Portainer API token rather than a general administrator token. 7. Rotate the existing token after remediation if it has ever been used over an untrusted network. 8. Consider certificate or public-key pinning for especially sensitive infrastructure. ]]>

T08 · Insecure Dependencies

Warning
Location
package.json:5
Finding
Python Dependencies Declared and Installed Through Unsafe, Inconsistent Mechanisms<![CDATA[ ## Vulnerability Details **File Location**: `package.json:5-8`; `README.md:15-18` **Vulnerability Type**: Dependency confusion, unpinned dependencies, and unsafe global package installation **Risk Level**: Medium ### Vulnerable Code `package.json` declares Python import names as npm dependencies: ```json "dependencies": { "requests": "^2.31.0", "urllib3": "^2.0.0" } ``` The README separately instructs users to install an unpinned Python dependency globally: ```dockerfile # Install Python 3 and pip RUN apt-get update && apt-get install -y python3 python3-pip # Install required Python libraries RUN pip3 install requests --break-system-packages ``` ### Technical Analysis The implementation is Python and imports the Python packages `requests` and `urllib3`. However, `package.json` causes npm-compatible tooling to interpret `requests` and `urllib3` as JavaScript packages from the npm registry, not the intended Python packages from PyPI. This may install unrelated components solely because they share the same names. No lockfile or integrity-controlled Python dependency manifest is present. The README then uses `pip3 install requests` without an exact version or hash and adds `--break-system-packages`, bypassing protections intended to prevent pip from modifying the operating system's managed Python environment. These mechanisms create several supply-chain and operational risks: - npm may install packages unrelated to the Python libraries used by the Skill. - Future Python package versions may be selected without review. - Builds are not reproducible because exact versions and artifact hashes are absent. - Global Python state may be modified in ways that conflict with operating-system packages or other applications. - A compromised upstream release or registry response would be consumed without hash verification. The audit did not establish that the referenced packages are malicious. The vulnerability is the unsafe and inconsistent dependency- ...[truncated 1789 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove Python package names from the npm `dependencies` section of `package.json`. 2. Add a Python dependency manifest such as `requirements.txt` or `pyproject.toml`. 3. Pin dependencies to reviewed versions rather than using broad or floating ranges. 4. Generate and verify cryptographic hashes for released artifacts. For example: ```text requests==<reviewed-version> --hash=sha256:<verified-hash> urllib3==<reviewed-version> --hash=sha256:<verified-hash> ``` 5. Install dependencies using `pip install --require-hashes -r requirements.txt`. 6. Use a dedicated virtual environment or isolated container layer instead of `--break-system-packages`. 7. Commit an appropriate lockfile and review dependency updates before merging them. 8. Use a trusted package index and retain installer logs or a software bill of materials for release builds. 9. Add automated dependency scanning for both Python and npm metadata. 10. If `package.json` is required only for OpenClaw metadata, keep it free of npm dependencies unless JavaScript packages are genuinely needed. ]]>
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
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (33)

Tainted flow: 'headers' from os.environ.get (line 129, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
try:
        token = get_portainer_api_token()
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/endpoints", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'headers' from os.environ.get (line 129, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
try:
        token = get_portainer_api_token()
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/stacks", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        stacks = response.json()
        if environment_id:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'headers' from os.environ.get (line 129, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
try:
        token = get_portainer_api_token()
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/stacks/{stack_id}", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        output_data = response.json()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'headers' from os.environ.get (line 129, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
# Also fetch the stack file content if possible
        try:
            file_response = requests.get(f"{PORTAINER_API_URL}/stacks/{stack_id}/file", headers=headers, timeout=10, verify=False)
            if file_response.status_code == 200:
                output_data["StackFileContent"] = file_response.json().get("StackFileContent", "")
        except:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'url' from os.environ.get (line 131, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
headers = {"X-API-Key": token, "Content-Type": "application/json"}
        url = f"{PORTAINER_API_URL}/stacks/create/standalone/string?endpointId={endpoint_id}"
        payload = {"name": name, "stackFileContent": stack_content, "env": []}
        response = requests.post(url, headers=headers, json=payload, timeout=30, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'url' from os.environ.get (line 131, credential/environment) → requests.put (network output)

Critical
Category
Data Flow
Content
"env": [],
            "prune": prune
        }
        response = requests.put(url, headers=headers, json=payload, timeout=30, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'url' from os.environ.get (line 131, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
url = f"{PORTAINER_API_URL}/endpoints/{environment_id}/docker{path}"
        
        if method.upper() == "GET":
            response = requests.get(url, headers=headers, params=payload, timeout=10, verify=False)
        elif method.upper() == "POST":
            response = requests.post(url, headers=headers, json=payload, timeout=10, verify=False)
        elif method.upper() == "DELETE":
Confidence
90% confidence
Finding
Unlike the other TT3 findings, this request is part of a generic raw Docker API proxy that accepts arbitrary user-supplied paths and forwards authenticated requests through Portainer. That greatly expands the trust boundary and can turn the skill into a general-purpose privileged API tunnel, enabling unauthorized or destructive Docker operations if exposed to untrusted prompts or users.

Tainted flow: 'url' from os.environ.get (line 131, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
if method.upper() == "GET":
            response = requests.get(url, headers=headers, params=payload, timeout=10, verify=False)
        elif method.upper() == "POST":
            response = requests.post(url, headers=headers, json=payload, timeout=10, verify=False)
        elif method.upper() == "DELETE":
            response = requests.delete(url, headers=headers, json=payload, timeout=10, verify=False)
        else:
Confidence
90% confidence
Finding
This POST path is part of the same unrestricted Docker API proxy and allows arbitrary authenticated write operations against Docker through Portainer. In practice, this can be abused to create containers, mount sensitive host paths, alter networks, or otherwise take over the managed environment.

Credential Access

High
Category
Privilege Escalation
Content
## Prerequisites

- An active [Portainer CE](https://www.portainer.io/) instance.
- An **API Access Token** from your Portainer user settings.
- OpenClaw installed and running.

## Installation
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## Prerequisites

- An active [Portainer CE](https://www.portainer.io/) instance.
- An **API Access Token** from your Portainer user settings.
- OpenClaw installed and running.

## Installation
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

High
Confidence
97% confidence
Finding
The skill exposes raw Docker API execution through Portainer and labels it only as 'Advanced' without explaining that it can directly control containers, images, networks, volumes, and potentially host-adjacent resources. In the context of infrastructure management, this is especially dangerous because a broad proxy endpoint can be used for highly privileged actions leading to outage, data destruction, lateral movement, or container escape enabling conditions.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The skill claims to manage Portainer environments and stacks, but it also exposes a generic raw Docker API proxy. That is significantly more powerful than the stated purpose and can enable full container-host compromise patterns, destructive actions, secret access, and persistence depending on Docker daemon privileges. In this skill context, that mismatch makes the capability especially dangerous because users may not realize they are granting near-administrative infrastructure control.

Missing User Warnings

High
Confidence
98% confidence
Finding
The generic Docker API executor permits potentially destructive operations with no warnings, confirmation, or policy gating. Because Docker APIs can create privileged containers, remove workloads, alter networking, and expose host resources, this is highly dangerous in an agent-exposed interface.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The README advertises deployment, removal, and raw Docker API execution capabilities without any guidance to require explicit user confirmation for destructive or low-level actions. In an agentic context, this increases the chance of accidental infrastructure changes, service disruption, or abuse of powerful functionality through prompt manipulation or ambiguous user requests.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill advertises high-impact capabilities involving environment access and network-reachable infrastructure operations, but it does not declare any explicit tool scope or permission boundaries. In a skill that can manage Portainer and proxy Docker API calls, missing scope metadata increases the risk of overbroad invocation, unsafe delegation, and misuse of privileged credentials.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill exposes a destructive operation, stack deletion, without any warning, confirmation requirement, or safety guidance. Because stack removal can cause service outage and data loss in live environments, describing it as a normal function without guardrails makes accidental or unauthorized destructive use more likely.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
try:
        token = get_portainer_api_token()
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/endpoints", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
Confidence
99% confidence
Finding
TLS certificate verification is explicitly disabled for requests carrying the Portainer API key. This allows man-in-the-middle interception or spoofing of the Portainer server, potentially exposing credentials and permitting tampering with responses and administrative actions.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
try:
        token = get_portainer_api_token()
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/stacks", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        stacks = response.json()
        if environment_id:
Confidence
99% confidence
Finding
Disabling TLS verification on authenticated API requests undermines the security of the entire Portainer session. An attacker on the network could impersonate the server, harvest the API key, and control the data returned to the skill.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
try:
        token = get_portainer_api_token()
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/stacks/{stack_id}", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        output_data = response.json()
Confidence
99% confidence
Finding
This insecure default affects stack inspection requests that include privileged authentication headers. With verify=False, the client cannot authenticate the server identity, enabling interception of sensitive stack metadata and credentials in transit.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
# Also fetch the stack file content if possible
        try:
            file_response = requests.get(f"{PORTAINER_API_URL}/stacks/{stack_id}/file", headers=headers, timeout=10, verify=False)
            if file_response.status_code == 200:
                output_data["StackFileContent"] = file_response.json().get("StackFileContent", "")
        except:
Confidence
99% confidence
Finding
Fetching stack file content over a connection that does not verify TLS certificates risks disclosure of full compose files, which may contain secrets, environment values, image references, and infrastructure details. Because this is admin-plane data, compromise impact is substantial.

Description-Behavior Mismatch

Medium
Confidence
85% confidence
Finding
The manifest emphasizes deployment, status checks, and network management, but the implementation also supports stack deletion. While deletion may be legitimate for an admin tool, omitting that destructive capability from the framing can mislead users and downstream policy about the true risk of invoking the skill.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
remove_stack performs an irreversible destructive action immediately, with no confirmation, dry-run, dependency check, or safeguard beyond a log message. In an agent setting, this increases the chance of accidental deletion due to prompt confusion, parameter mix-ups, or malicious instruction injection through user content.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
try:
        token = get_portainer_api_token()
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.delete(f"{PORTAINER_API_URL}/stacks/{stack_id}", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        print(f"Successfully removed stack {stack_id}.", flush=True)
        return {"status": "success"}
Confidence
99% confidence
Finding
A destructive delete request is sent over a TLS connection with certificate validation disabled. That means an attacker able to intercept traffic could potentially spoof responses or capture the API key used to authorize deletions, compounding the risk.

External Transmission

Medium
Category
Data Exfiltration
Content
headers = {"X-API-Key": token, "Content-Type": "application/json"}
        url = f"{PORTAINER_API_URL}/stacks/create/standalone/string?endpointId={endpoint_id}"
        payload = {"name": name, "stackFileContent": stack_content, "env": []}
        response = requests.post(url, headers=headers, json=payload, timeout=30, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
headers = {"X-API-Key": token, "Content-Type": "application/json"}
        url = f"{PORTAINER_API_URL}/stacks/create/standalone/string?endpointId={endpoint_id}"
        payload = {"name": name, "stackFileContent": stack_content, "env": []}
        response = requests.post(url, headers=headers, json=payload, timeout=30, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
Confidence
99% confidence
Finding
Stack deployment sends potentially sensitive infrastructure configuration over a connection with verify=False. This can leak secrets embedded in compose content and let an attacker alter or replay deployment requests.

Static analysis

No suspicious patterns detected.