Back to skill

Security audit

MeshOps Control Plane

Security checks for vulnerabilities and agentic risk

Overview

This skill fits a mesh-control purpose, but it gives task-driven authority over plugin installs, skill deployment, gateway restarts, and command execution with weak scoping and authorization.

Install only in an operator-controlled OpenClaw environment. Do not enable high-risk gates or accept task files from untrusted sources until caller identity is authenticated outside the task body, plugin and skill sources are allowlisted or signed, archive extraction is hardened, and task/path fields are strictly validated.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
src/handler.py:20
Finding
Spoofable Caller Authorization Allows Impersonation of Privileged Operators<![CDATA[ ## Vulnerability Details **File Location**: `src/handler.py:20-45` **Vulnerability Type**: Untrusted caller identity used as an authorization credential **Risk Level**: High ### Vulnerable Code ```python def _allowed_callers() -> set[str]: raw = os.environ.get("OPENCLAW_ALLOWED_CALLERS", "architect,chief-of-staff") out = set() for part in raw.split(","): v = part.strip() if v: out.add(v) return out def authorize(task: dict) -> tuple[bool, str]: action = str(task.get("action", "")).strip() caller = str(task.get("caller", "")).strip() if not caller: return False, "missing caller" allowed = _allowed_callers() if caller not in allowed: return False, f"caller '{caller}' not authorized" if action in HIGH_RISK_ACTIONS: allow_high_risk = os.environ.get("OPENCLAW_ALLOW_HIGH_RISK", "0") == "1" if not allow_high_risk: return False, f"action '{action}' blocked (set OPENCLAW_ALLOW_HIGH_RISK=1 to enable)" return True, "ok" ``` The task schema also defines `caller` as an ordinary task-controlled string: ```json { "properties": { "task_id": {"type":"string"}, "action": {"type":"string"}, "params": {"type":"object"}, "caller": {"type":"string"}, "correlation_id": {"type":"string"} } } ``` ### Technical Analysis The authorization decision treats the `caller` value embedded in the submitted JSON document as proof of identity. Because the same party submitting or modifying a task can choose this field, an attacker can claim to be any name in `OPENCLAW_ALLOWED_CALLERS`, including the default privileged identities `architect` and `chief-of-staff`. No cryptographic signature, authenticated transport identity, trusted gateway assertion, token, file ownership check, or other mechanism binds the claimed caller to a verified principal. The handler also does not validate the task against `schemas/task.schema.json`, although schema ...[truncated 1814 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not accept `caller` in the task body as an authentication credential. 2. Derive caller identity from a trusted execution context, such as: - Mutually authenticated transport metadata. - A verified gateway identity. - A signed task envelope using a trusted caller key. - A protected local IPC mechanism with operating-system peer credentials. 3. Bind the verified identity to the task body, action, parameters, timestamp, and nonce. 4. Add expiration and replay protection to signed or authenticated tasks. 5. Reject discrepancies between informational caller fields and the authenticated identity. 6. Apply authorization after authentication and before any filesystem writes or action dispatch. 7. Validate tasks against a strict schema with `additionalProperties: false`, action-specific parameter schemas, size limits, and identifier constraints. 8. Replace privileged default caller names with an explicit fail-closed configuration where appropriate. 9. Restrict task-file permissions and verify file ownership if filesystem-based task ingestion remains supported. ]]>

T03 · Remote Payload Retrieval and Execution

Error
Location
actions/setup-ansible-plugin.sh:9
Finding
Attacker-Selected Plugin Installation Bypasses High-Risk Action Gating<![CDATA[ ## Vulnerability Details **File Location**: `src/handler.py:12-12, 40-43`; `actions/setup-ansible-plugin.sh:9-46` **Vulnerability Type**: Unpinned third-party code installation without high-risk approval **Risk Level**: Critical ### Vulnerable Code The handler excludes plugin installation from its high-risk action set: ```python HIGH_RISK_ACTIONS = {"run-cmd", "deploy-skill"} ``` Only actions in that set receive the global high-risk check: ```python if action in HIGH_RISK_ACTIONS: allow_high_risk = os.environ.get("OPENCLAW_ALLOW_HIGH_RISK", "0") == "1" if not allow_high_risk: return False, f"action '{action}' blocked (set OPENCLAW_ALLOW_HIGH_RISK=1 to enable)" ``` The plugin action accepts a task-controlled source and reference: ```bash SOURCE=$(jq -r '.params.source // "github"' "$TASK_FILE") PLUGIN_REF=$(jq -r '.params.plugin_ref // ""' "$TASK_FILE") RUN_SETUP=$(jq -r '.params.run_setup // true' "$TASK_FILE") VERIFY_STATUS=$(jq -r '.params.verify_status // true' "$TASK_FILE") RESTART_GATEWAY=$(jq -r '.params.restart_gateway // false' "$TASK_FILE") INSTALL_TARGET="" case "$SOURCE" in github) INSTALL_TARGET=${PLUGIN_REF:-likesjx/openclaw-plugin-ansible} ;; npm) INSTALL_TARGET=${PLUGIN_REF:-@jaredlikes/openclaw-plugin-ansible} ;; path) if [ -z "$PLUGIN_REF" ]; then echo "path source requires params.plugin_ref" | tee -a "$LOG" >&2 exit 2 fi INSTALL_TARGET="$PLUGIN_REF" ;; *) echo "unsupported source: $SOURCE" | tee -a "$LOG" >&2 exit 2 ;; esac run_cmd "plugin-install" openclaw plugins install "$INSTALL_TARGET" ``` ### Technical Analysis Plugin installation is a code-acquisition and code-execution operation. The task can select a GitHub repository, npm package, or local path, but the implementation does not: - Require `OPENCLAW_ALLOW_HIGH_RISK`. - Require a dedicated plugin-installation gate. - Restrict the reference to an approved package or repository. - Pin the ...[truncated 2160 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add `setup-ansible-plugin` to `HIGH_RISK_ACTIONS`. 2. Require a separate, default-disabled gate such as `OPENCLAW_ALLOW_PLUGIN_INSTALL=1`. 3. Require authenticated, non-spoofable operator approval for every new plugin identity or version. 4. Replace arbitrary `plugin_ref` support with an allowlist of exact package names and repositories. 5. Pin GitHub sources to verified immutable commit hashes and npm sources to exact versions plus trusted integrity digests. 6. Verify signed release provenance against independently configured publisher keys. 7. Reject local-path installation unless the resolved path is beneath a trusted, operator-controlled directory. 8. Install plugins in a sandbox or staging environment before gateway activation. 9. Disable dependency lifecycle scripts where supported and inspect plugin contents before loading. 10. Separate installation from activation and gateway restart, requiring an additional approval for each stage. ]]>

T03 · Remote Payload Retrieval and Execution

Error
Location
actions/deploy-skill.sh:6
Finding
Downloaded Skill Archive Can Execute Task-Selected Remote Code<![CDATA[ ## Vulnerability Details **File Location**: `actions/deploy-skill.sh:6-8, 41-63` **Vulnerability Type**: Remote payload retrieval followed by unsandboxed execution **Risk Level**: Critical ### Vulnerable Code ```bash URL=$(jq -r '.params.artifact_url // ""' "$TASK_FILE") NAME=$(jq -r '.params.name // ""' "$TASK_FILE") SHA_EXPECT=$(jq -r '.params.sha // ""' "$TASK_FILE") ``` ```bash echo "Downloading $URL" | tee -a "$LOG" curl -fsSL "$URL" -o "$ARCHIVE" if command -v sha256sum >/dev/null 2>&1; then SHA_ACT=$(sha256sum "$ARCHIVE" | awk '{print $1}') else SHA_ACT=$(shasum -a 256 "$ARCHIVE" | awk '{print $1}') fi if [ "$SHA_EXPECT" != "$SHA_ACT" ]; then echo "SHA mismatch expected=$SHA_EXPECT actual=$SHA_ACT" | tee -a "$LOG" >&2 exit 4 fi DEST="/opt/openclaw/skills/$NAME" mkdir -p "$DEST" echo "Extracting into $DEST" | tee -a "$LOG" tar -xzf "$ARCHIVE" -C "$DEST" if [ -x "$DEST/test_smoke.sh" ]; then echo "Running smoke test" | tee -a "$LOG" (cd "$DEST" && ./test_smoke.sh) || { echo "Smoke failed" | tee -a "$LOG" >&2; exit 5; } fi ``` ### Technical Analysis The script downloads a task-selected HTTPS resource, extracts it, and executes `test_smoke.sh` if the archive marks that file executable. Although a SHA-256 digest is required, the expected digest is supplied in the same untrusted task as the URL. This protects against accidental corruption or changes relative to the task but does not establish publisher trust. An attacker can create a malicious archive, calculate its SHA-256 digest, and submit both values. HTTPS similarly protects transport confidentiality and integrity but does not prove that the artifact publisher is authorized. Redirects are followed through `curl -L`, and there is no host allowlist, signature verification, provenance validation, content review, or execution sandbox. ### Attack Path 1. Create an archive containing an executable file named `test_smoke.sh`. 2. Put arbitrary commands in that file, for example ...[truncated 1425 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not execute any script supplied by a downloaded deployment archive. 2. Move smoke testing to an isolated build or validation environment with: - No production credentials. - No host filesystem access. - Restricted network access. - Read-only inputs and disposable storage. 3. Restrict artifact URLs to approved registries and exact trusted origins. 4. Verify a publisher signature or transparency-log provenance independently of task-supplied data. 5. Pin artifacts through an operator-controlled manifest that maps approved identities and versions to digests. 6. Require authenticated human approval for the exact artifact digest. 7. Download without credentials unless explicitly required, and prevent credential forwarding across redirects. 8. Stage the artifact as an unprivileged user, validate it, and only then promote static approved files. 9. Record the verified publisher identity, immutable digest, approval reference, and validation result in the deployment audit log. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
actions/deploy-skill.sh:5
Finding
Unvalidated Deployment Paths and Archive Entries Can Escape the Intended Skill Directory<![CDATA[ ## Vulnerability Details **File Location**: `actions/deploy-skill.sh:5-13, 37-39, 55-59` **Vulnerability Type**: Path traversal and unsafe archive extraction **Risk Level**: High ### Vulnerable Code ```bash TASK_FILE="$1" TASK_ID=$(jq -r '.task_id' "$TASK_FILE") URL=$(jq -r '.params.artifact_url // ""' "$TASK_FILE") NAME=$(jq -r '.params.name // ""' "$TASK_FILE") SHA_EXPECT=$(jq -r '.params.sha // ""' "$TASK_FILE") ARTROOT=${OPENCLAW_ARTIFACT_ROOT:-/var/lib/openclaw/artifacts} mkdir -p "$ARTROOT" LOG="$ARTROOT/${TASK_ID}-deploy.log" OUT="$ARTROOT/${TASK_ID}-deploy.json" ``` ```bash TMP_DIR="/tmp/${TASK_ID}" mkdir -p "$TMP_DIR" ARCHIVE="$TMP_DIR/artifact.tar.gz" ``` ```bash DEST="/opt/openclaw/skills/$NAME" mkdir -p "$DEST" echo "Extracting into $DEST" | tee -a "$LOG" tar -xzf "$ARCHIVE" -C "$DEST" ``` ### Technical Analysis Both `TASK_ID` and `NAME` are incorporated into filesystem paths without validation or canonical containment checks. A value such as `../../target` can cause the resulting path to resolve outside the intended artifact, temporary, or Skill directories. Quoting prevents word splitting and shell metacharacter expansion, but it does not prevent filesystem traversal through `..` components or absolute paths. The archive is passed directly to `tar` without first inspecting members for: - Absolute paths. - Parent-directory traversal. - Symbolic or hard links targeting locations outside the extraction root. - Special files. - Unexpected ownership or permission metadata. Some `tar` implementations reject or sanitize certain traversal forms, but the script does not enforce portable, explicit safety checks. The task-controlled destination traversal remains independently exploitable wherever the process has suitable permissions. ### Attack Path One direct destination traversal path is: 1. Submit a deployment task with a name such as `../../tmp/attacker-destination`. 2. The script constructs: ```text /opt/openclaw/skills/.. ...[truncated 1243 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce strict identifier patterns before constructing paths, for example: - `task_id`: `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$` - `name`: `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$` 2. Explicitly reject `/`, `\`, `..`, control characters, empty values, and absolute paths. 3. Generate temporary directories with `mktemp -d` under a fixed trusted parent rather than using `"/tmp/${TASK_ID}"`. 4. Resolve destination paths canonically and verify that they remain beneath `/opt/openclaw/skills`. 5. List and validate every archive entry before extraction. 6. Reject absolute paths, parent traversal, symbolic links, hard links, devices, FIFOs, sockets, and unexpected permission bits. 7. Extract as an unprivileged account into a newly created staging directory. 8. Use restrictive creation permissions and avoid preserving archive ownership. 9. Promote only validated regular files into the final destination. 10. Refuse deployment if the destination already exists unless an authenticated, atomic upgrade workflow is used. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
actions/run-cmd.sh:4
Finding
Unvalidated Task IDs Permit Artifact File Path Traversal Across Actions<![CDATA[ ## Vulnerability Details **File Location**: `actions/run-cmd.sh:4-13` **Vulnerability Type**: Attacker-controlled output path construction **Risk Level**: High ### Vulnerable Code A representative affected action is: ```bash TASK_FILE="$1" TASK_ID=$(jq -r '.task_id' "$TASK_FILE") CMD=$(jq -r '.params.cmd // ""' "$TASK_FILE") TIMEOUT_SECONDS=$(jq -r '.params.timeout // 30' "$TASK_FILE") ARTROOT=${OPENCLAW_ARTIFACT_ROOT:-/var/lib/openclaw/artifacts} mkdir -p "$ARTROOT" OUT="$ARTROOT/${TASK_ID}-run-cmd.json" STDOUT_LOG="$ARTROOT/${TASK_ID}-stdout.log" STDERR_LOG="$ARTROOT/${TASK_ID}-stderr.log" ``` The same unsafe pattern appears in: - `actions/collect-logs.sh:4-8` - `actions/deploy-skill.sh:4-13` - `actions/preflight.sh:4-8` - `actions/setup-ansible-plugin.sh:4-16` For example: ```bash TASK_ID=$(jq -r ".task_id" "$TASK_FILE") ARTROOT=${OPENCLAW_ARTIFACT_ROOT:-/var/lib/openclaw/artifacts} mkdir -p "$ARTROOT" OUT="$ARTROOT/${TASK_ID}-logs.txt" ``` ### Technical Analysis The task-controlled `task_id` is inserted into multiple output filenames without validation. Shell quoting correctly prevents word splitting and glob expansion, but path separators and `..` components remain active when the operating system resolves the path. Consequently, an attacker can cause an action to write stdout, stderr, status JSON, preflight information, deployment logs, or collected system information outside `OPENCLAW_ARTIFACT_ROOT`. The data written is partly attacker-influenced. For example, command output may be redirected to a traversal-selected location, and generated JSON includes task-controlled fields. Existing files can be truncated because shell redirection uses `>`. ### Attack Path 1. Submit a task with a traversal-bearing identifier, such as: ```json { "task_id": "../../../tmp/attacker", "caller": "architect", "action": "run-cmd", "params": { "cmd": "openclaw status" } } ``` 2. The action constructs paths de ...[truncated 1262 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate `task_id` once in the trusted Python handler before dispatch. 2. Permit only a conservative identifier character set and length. 3. Pass a handler-generated opaque artifact identifier to scripts instead of reusing task-controlled input. 4. Resolve each output path and verify that its parent and canonical path remain under `OPENCLAW_ARTIFACT_ROOT`. 5. Open output files with secure creation semantics that prevent unintended overwrite and symbolic-link following. 6. Run the dispatcher with a dedicated, unprivileged account whose write permissions are limited to the artifact directory. 7. Apply the same correction consistently to `collect-logs.sh`, `deploy-skill.sh`, `preflight.sh`, `run-cmd.sh`, and `setup-ansible-plugin.sh`. 8. Add regression tests covering `../`, absolute paths, encoded separators, control characters, long identifiers, and symbolic-link attacks. ]]>
Vulnerability Patterns
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (14)

Missing User Warnings

High
Confidence
99% confidence
Finding
The script executes test_smoke.sh directly from the newly deployed artifact, meaning any party controlling the artifact contents can achieve arbitrary code execution on the host as the deploy user. The SHA256 check does not reduce the risk if an attacker can submit both the artifact URL and matching hash in the task file, making this effectively an explicit remote code execution path.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill documents an admin-capable `ansible_delete_messages` tool as an emergency purge mechanism but does not pair it with explicit destructive-operation warnings, confirmation requirements, or recovery limitations. In a distributed coordination system where shared messages are durable state and part of operational evidence, this omission increases the risk of accidental or socially engineered deletion of important audit trails and active work context.

Session Persistence

Medium
Category
Rogue Agent
Content
| Tool | Purpose |
|------|---------|
| `ansible_delegate_task` | Create task for another node/agent set |
| `ansible_claim_task` | Claim pending task |
| `ansible_update_task` | Update task status/progress |
| `ansible_complete_task` | Complete task and notify requester |
Confidence
80% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
The retention/pruning feature is described as routine lifecycle management without clearly warning that configuration changes can permanently remove closed-task history and coordination evidence. Because this skill emphasizes durable shared state and auditability, understated pruning semantics can lead to unintended data loss, weakened forensics, or premature deletion of operational records.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The script extracts a remotely downloaded archive directly into the skills directory after only verifying a supplied hash, but it does not validate archive contents for path traversal, symlinks, ownership, or dangerous file placement. A crafted tarball can overwrite unexpected files within the destination tree or plant executable content that will later be trusted as part of a deployed skill.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script serializes multiple environment-derived values, including caller allowlists and action gating flags, into a persistent artifact file. While this is likely intended for diagnostics, it can expose internal policy configuration and operational controls to any actor who can read artifacts, which may aid reconnaissance or policy circumvention attempts.

Skill Enumeration

Medium
Category
Agent Snooping
Content
"action": "scaffold",
    "branch": "skills/feature/secrets-mgmt",
    "files_created": [
      "skills/openclaw-skill-ansible/subskills/secrets-mgmt/SKILL.md",
      "playbooks/secrets-mgmt.yml",
      "roles/vault/tasks/main.yml"
    ],
Confidence
80% confidence
Finding
Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.

Session Persistence

Medium
Category
Rogue Agent
Content
"status": "failed",
  "issues": [
    "Git branch creation failed: branch already exists",
    "Cannot write to repository: permission denied"
  ]
}
```
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Skill Enumeration

Medium
Category
Agent Snooping
Content
### Skill Loading

OpenClaw loads `SKILL.md` files from:
- `~/.openclaw/workspace/skills/ansible/SKILL.md` — core coordination skill
- `~/.openclaw/workspace/skills/ansible/skills/*/SKILL.md` — skill catalog entries

Agents receive skill context when:
Confidence
80% confidence
Finding
Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.

Vague Triggers

Medium
Confidence
91% confidence
Finding
The requester-side auto-invocation rule is intentionally broad: agents are told to delegate any work that 'falls under a registered capability' before proceeding. In a shared multi-agent mesh, this can cause over-delegation of sensitive, user-scoped, or partially understood tasks to any agent advertising a matching capability, increasing the chance of unintended data exposure, privilege boundary crossing, or unsafe task fan-out. The surrounding skill context makes this more dangerous because capability registration is automatic and discovery is global via shared Yjs state, so the triggering surface is large and dynamic.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The executor-side rule says to claim the first unclaimed task matching a registered capability and process it before any other work, but the matching criteria and safety gates are underspecified. In practice, any agent with that capability may automatically claim and execute tasks without validating origin, authorization, scope, or whether the task is actually appropriate for that executor, creating a confused-deputy risk and enabling unintended task handling. This context increases danger because tasks are sourced from shared replicated state and capability loading auto-registers executors, so accidental or unauthorized claiming can happen across gateways.

Intent-Code Divergence

Medium
Confidence
91% confidence
Finding
The module documentation presents the component as a secure dispatcher, but the implementation is fundamentally a shell-script launcher whose behavior can include explicitly high-risk actions. That is not merely incomplete documentation: the 'secure' characterization conflicts with the fact that the code enables command and deployment execution when environment gates permit it.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return 2

    # Run script directly with argv, never shell interpolation.
    result = subprocess.run(["/bin/bash", str(script), task_path], check=False)
    return int(result.returncode)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Intent-Code Divergence

Low
Confidence
78% confidence
Finding
This file is documentation, so the only applicable mismatch class is documentation-to-documented-capability divergence. The guidance at L97-L98 frames operations as preferring managed, auditable paths and explicitly gating high-risk execution, yet the same document enumerates a broad set of direct state-mutating commands such as `messages-delete`, admin setters, retention changes, token issuance, and agent rebind/disable flows. While not a hard contradiction in code, the notes present a narrower operational posture than the capabilities actually listed.

Static analysis

No suspicious patterns detected.