Back to skill

Security audit

Portainer

Security checks across malware telemetry and agentic risk

Overview

This appears to be a real Portainer management skill, but it gives an agent broad Docker administration power over an insecure TLS setup with limited safety guardrails.

Install only if you intentionally want an agent to administer Portainer and Docker. Use a least-privilege Portainer token, avoid production targets unless you add confirmations or allowlists, enable real TLS certificate verification, and treat raw Docker API, deploy, update, prune, and remove operations as actions that can interrupt services or delete resources.

SkillSpector

By NVIDIA
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • 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 (22)

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
89% confidence
Finding
response = requests.get(url, headers=headers, params=payload, timeout=10, verify=False)

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
89% confidence
Finding
response = requests.post(url, headers=headers, json=payload, timeout=10, verify=False)

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill exposes sensitive capabilities involving environment-backed secrets and network access to the Portainer API, including raw Docker API proxying, but does not declare permissions or constraints. This weakens reviewability and safety boundaries, making it easier for an agent or operator to invoke infrastructure-changing actions without clear authorization expectations.

Description-Behavior Mismatch

High
Confidence
96% confidence
Finding
The manifest advertises environment and stack management, but the implementation includes a generic raw Docker API executor that can perform far broader actions than users would reasonably expect. This capability mismatch is dangerous because it hides privileged infrastructure control behind an understated interface, increasing the chance of misuse or prompt-driven abuse.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
Arbitrary Docker command execution via the Portainer proxy provides a highly privileged remote administration surface that goes well beyond listing or managing stacks. An attacker or misled agent could use it to create privileged containers, mount host filesystems, extract secrets, disrupt services, or gain effective host-level control depending on Docker configuration.

Missing User Warnings

Medium
Confidence
82% confidence
Finding
The README promotes high-impact operational actions such as deploying stacks, removing stacks, and executing raw Docker commands, but it does not prominently warn about service disruption, data loss, or the blast radius of these actions. In the context of an agent skill that can act on user prompts, this increases the chance of unsafe or accidental destructive operations against production infrastructure.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
The skill documents stack deletion as a simple callable action without any warning, confirmation, or guardrails. In an infrastructure-management context, this can lead to accidental or overly-trusting destructive actions that remove running services and cause outages or data loss.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The remove_stack operation performs an irreversible destructive action without any built-in confirmation, dry-run support, or warning mechanism. In an agentic context, this raises the risk of accidental deletion from prompt misunderstanding, ambiguous user intent, or automation mistakes.

Missing User Warnings

High
Confidence
97% confidence
Finding
The generic Docker API executor exposes dangerous remote operations without safety interlocks, despite being capable of destructive actions like deleting containers, networks, or volumes and creating privileged workloads. In an LLM/agent setting, lack of warnings and guardrails significantly increases the probability of unintended infrastructure damage.

External Transmission

Medium
Category
Data Exfiltration
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
85% confidence
Finding
requests.post(url, headers=headers, json=

Unpinned Dependencies

Low
Category
Supply Chain
Content
"version": "1.0.0",
  "description": "Manages Portainer CE environments and stacks via HTTP API.",
  "dependencies": {
    "requests": "^2.31.0",
    "urllib3": "^2.0.0"
  },
  "openclaw": {
Confidence
88% confidence
Finding
"requests": "^2.31.0"

Unpinned Dependencies

Low
Category
Supply Chain
Content
"description": "Manages Portainer CE environments and stacks via HTTP API.",
  "dependencies": {
    "requests": "^2.31.0",
    "urllib3": "^2.0.0"
  },
  "openclaw": {
    "entrypoint": "scripts/portainer_manager.py"
Confidence
88% confidence
Finding
"urllib3": "^2.0.0"

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
verify=False

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
verify=False

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
verify=False

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
verify=False

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
verify=False

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
verify=False

Unsafe Defaults

Medium
Category
Tool Misuse
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
99% confidence
Finding
verify=False

Unsafe Defaults

Medium
Category
Tool Misuse
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
99% confidence
Finding
verify=False

Unsafe Defaults

Medium
Category
Tool Misuse
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
99% confidence
Finding
verify=False

Unsafe Defaults

Medium
Category
Tool Misuse
Content
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:
            raise ValueError(f"Unsupported method: {method}")
Confidence
99% confidence
Finding
verify=False

VirusTotal

64/64 vendors flagged this skill as clean.

View on VirusTotal

Static analysis

No suspicious patterns detected.