Back to skill

Security audit

Openclaw Skill Ansible

Security checks for vulnerabilities and agentic risk

Overview

This skill is a real mesh administration tool, but it gives task-controlled inputs too much power to install or execute code on an OpenClaw gateway.

Review before installing. This should only run in an administrator-controlled OpenClaw environment where task creation is authenticated and package sources are trusted. Do not enable deployment or plugin setup for untrusted callers; require signed or allowlisted artifacts, pin plugin identities and versions, stage extraction safely, and avoid automatic execution of package-provided smoke tests.

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 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
Findings (4)

T03 · Remote Payload Retrieval and Execution

Error
Location
actions/deploy-skill.sh:19
Finding
Attacker-Supplied Remote Archive Is Downloaded and Automatically Executed<![CDATA[ ## Vulnerability Details **File Location**: `actions/deploy-skill.sh:19-64` **Vulnerability Type**: Remote payload retrieval and execution **Risk Level**: Critical ### Vulnerable Code ```bash if [[ "$URL" != https://* ]]; then echo "artifact_url must use https" | tee -a "$LOG" >&2 exit 2 fi if [ -z "$SHA_EXPECT" ]; then echo "sha is required and must be sha256" | tee -a "$LOG" >&2 exit 2 fi if [[ ! "$SHA_EXPECT" =~ ^[a-fA-F0-9]{64}$ ]]; then echo "sha must be a 64-char hex digest" | tee -a "$LOG" >&2 exit 2 fi if [ "${OPENCLAW_ALLOW_DEPLOY_SKILL:-0}" != "1" ]; then echo "deploy-skill disabled: set OPENCLAW_ALLOW_DEPLOY_SKILL=1" | tee -a "$LOG" >&2 exit 3 fi TMP_DIR="/tmp/${TASK_ID}" mkdir -p "$TMP_DIR" ARCHIVE="$TMP_DIR/artifact.tar.gz" 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 deployment task controls both `params.artifact_url` and `params.sha`. The script only requires an HTTPS URL and a syntactically valid SHA-256 digest. Because the expected digest is supplied by the same untrusted task as the archive URL, it provides transfer-integrity checking but no publisher authentication. An attacker can therefore host an arbitrary archive, calculate its SHA-256 digest, and provide both values. After verification, the archive is extracted into the persistent Skill directory. If it c ...[truncated 1554 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict artifact URLs to an explicit allowlist of trusted repositories and expected URL formats. 2. Pin approved artifacts in server-side configuration rather than accepting both the URL and digest from the task. 3. Require a detached cryptographic signature and verify it against pinned publisher keys. 4. Bind the artifact name, version, URL, digest, and publisher identity into the signed manifest. 5. Remove automatic execution of archive-provided `test_smoke.sh`. 6. Run any necessary package validation inside a disposable sandbox with no secrets, network access, or host write access. 7. Extract to a staging directory, validate the complete package, and deploy atomically only after approval. 8. Execute deployment using a dedicated low-privilege account with narrowly scoped write access. ]]>

T08 · Insecure Dependencies

Error
Location
actions/setup-ansible-plugin.sh:25
Finding
Arbitrary Plugin Sources Can Be Installed Without High-Risk Authorization<![CDATA[ ## Vulnerability Details **File Location**: `actions/setup-ansible-plugin.sh:25-46`; authorization omission at `src/handler.py:11,40-44` **Vulnerability Type**: Unsafe dependency installation and missing privileged-action gate **Risk Level**: Critical ### Vulnerable Code ```bash 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" ``` The dispatcher only treats two other actions as high risk: ```python HIGH_RISK_ACTIONS = {"run-cmd", "deploy-skill"} ``` ```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)" ``` ### Technical Analysis `params.plugin_ref` can designate an arbitrary GitHub source, npm package, or local filesystem path. The selected target is passed directly to `openclaw plugins install`. There is no allowlist for package identities, no immutable commit or version requirement, no checksum or publisher-signature verification, and no dedicated approval artifact. This exposes the gateway to repository takeover, compromised package releases, dependency confusion, malicious local paths, and directly attacker-selected components. Although installing a plugin introduces executable code into the OpenClaw gateway, `setup-ansible-plugin` is absent from `HIGH_RISK_ACTIONS`. It therefore bypasses the global `OPENCLAW_ALLOW_HIGH_RISK` check applied to `run-cmd` and `deploy-skill`. The sam ...[truncated 1119 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add `setup-ansible-plugin` to `HIGH_RISK_ACTIONS`. 2. Introduce a dedicated plugin-installation gate in addition to the global high-risk gate. 3. Require explicit, verifiable human approval before installation or gateway restart. 4. Allowlist exact plugin identities and approved source registries. 5. Require immutable versions or Git commit identifiers; reject branches, moving tags, and unversioned packages. 6. Verify package checksums and publisher signatures against values stored outside task-controlled input. 7. Disable the `path` source unless the resolved path is inside an administrator-controlled directory. 8. Resolve and validate local paths to prevent symlink or traversal-based source substitution. 9. Scan and stage plugins in an isolated environment before installation. 10. Run the gateway and installer with separate least-privilege accounts. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
src/handler.py:29
Finding
Caller Authorization Can Be Spoofed Through Untrusted Task JSON<![CDATA[ ## Vulnerability Details **File Location**: `src/handler.py:29-44` **Vulnerability Type**: Authentication and authorization bypass **Risk Level**: Critical ### Vulnerable Code ```python 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" ``` ### Technical Analysis The authorization decision trusts the `caller` string inside the same task JSON that contains the requested action and parameters. There is no evidence that this field is cryptographically signed, bound to an authenticated transport identity, issued by a trusted dispatcher, or verified using a gateway token. Consequently, possession of the ability to create or modify a task file is equivalent to possession of an allowlisted identity. An attacker can simply set `caller` to the default value `architect` or `chief-of-staff`. The environment-based action gates reduce exposure for two actions when disabled, but they do not authenticate the task origin. Other sensitive operations, particularly plugin installation, remain reachable after caller spoofing. ### Attack Path 1. The attacker gains any route for supplying or modifying the JSON file passed to `handler.py`. 2. The attacker sets `caller` to an entry from `OPENCLAW_ALLOWED_CALLERS`, such as `architect`. 3. The attacker selects an action and attacker-controlled parameters. 4. `authorize()` performs only a string-membership comparison. 5. The forged identity is accepted. 6. The dispatcher invokes the c ...[truncated 567 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not derive caller identity from a field supplied by the task itself. 2. Bind tasks to an authenticated transport session or trusted gateway-issued principal. 3. Require each task to carry a digital signature from an approved caller key. 4. Sign the complete canonical task representation, including action, parameters, task ID, expiry, and nonce. 5. Enforce timestamp and nonce-based replay protection. 6. Validate that the verified signing identity is authorized for the specific action, not merely globally allowlisted. 7. Keep high-risk action approval separate from caller authentication. 8. Restrict task-file ownership and permissions and reject files writable by untrusted users. 9. Log the verified principal and approval reference for every privileged action. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
actions/deploy-skill.sh:35
Finding
Unvalidated Identifiers and Tar Entries Permit Filesystem Path Escape<![CDATA[ ## Vulnerability Details **File Location**: `actions/deploy-skill.sh:35-61` **Vulnerability Type**: Path traversal, unsafe temporary directory use, and unsafe archive extraction **Risk Level**: High ### Vulnerable Code ```bash TMP_DIR="/tmp/${TASK_ID}" mkdir -p "$TMP_DIR" ARCHIVE="$TMP_DIR/artifact.tar.gz" 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" ``` The same unvalidated task identifier is used to construct output paths in other actions, for example: ```bash TASK_ID=$(jq -r ".task_id" "$TASK_FILE") OUT="$ARTROOT/${TASK_ID}-logs.txt" ``` ### Technical Analysis `TASK_ID` and `NAME` are inserted into filesystem paths without restricting them to safe basename characters or verifying that the resolved paths remain beneath their intended roots. Values containing `../`, absolute-path-like components, or crafted separators can therefore escape `/tmp`, the artifact directory, or `/opt/openclaw/skills`. The tar archive is extracted without inspecting member names. Malicious archives may contain traversal entries, absolute paths, symbolic links, hard links, or special files. Depending on the installed tar implementation and its protections, these entries may overwrite or create files outside the intended destination. The predictable `/tmp/${TASK_ID}` directory also creates symlink and pre-creation risks when local untrusted users can manipulate `/tmp`. ### Attack Path 1. The attacker submits a task with a malicious `task_id` or deployment `name`, such as a value containing `../`. 2. The script concate ...[truncated 1082 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate `task_id` and `name` against a strict allowlist such as `^[A-Za-z0-9._-]+$`. 2. Reject empty values, `.` and `..`, path separators, control characters, and excessively long identifiers. 3. Resolve every constructed path and verify that it remains beneath the expected root before creating or writing files. 4. Replace predictable temporary directories with `mktemp -d` and set restrictive permissions. 5. Add cleanup traps to remove staging directories securely. 6. List and validate all archive members before extraction. 7. Reject absolute paths, `..` components, symbolic links, hard links, device nodes, FIFOs, and other special entries. 8. Extract as a dedicated low-privilege account into a new staging directory. 9. Use safe extraction APIs that enforce destination containment rather than invoking unrestricted `tar`. 10. Apply the same identifier and containment checks to output paths in `collect-logs.sh`, `run-cmd.sh`, and `preflight.sh`. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (12)

Context-Inappropriate Capability

High
Confidence
94% confidence
Finding
This script downloads a remote archive, extracts it into the live skills directory, and then executes a shipped script from that artifact. Even though HTTPS, a SHA-256 check, and an environment gate are present, the design still intentionally enables deployment and execution of externally supplied code, which is dangerous if the task input, artifact source, or expected hash can be influenced by an attacker.

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
80% confidence
Finding
This code creates a destination under /opt/openclaw/skills and extracts downloaded content into it, which is a file-writing operation that changes the local system state. While the script logs that extraction is happening, there is no user-facing warning, confirmation, or inline documentation explaining that running the skill will install files onto the host.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script executes test_smoke.sh directly from the deployed artifact, meaning arbitrary code from the package runs on the host during deployment. The prior SHA check only proves the file matches the provided digest; it does not establish that the artifact is trustworthy, so a malicious or compromised artifact can execute commands with the deployer's privileges.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The script installs a plugin and may run setup and restart gateway operations, which can change system state and affect service availability. Although commands are logged, there is no comment, docstring, or explicit warning in the file disclosing these safety-impacting actions before execution.

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.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This shell script performs safety-relevant actions by moving the source skill into a destination directory and creating a git commit, which can alter user files and repository history. While there is a brief comment and a final success echo, there is no confirmation prompt or explicit user-facing warning before these irreversible changes occur.

Intent-Code Divergence

Medium
Confidence
78% confidence
Finding
The docstring makes an affirmative security claim, but the implementation's main behavior is to invoke action-specific shell scripts and optionally allow explicitly marked high-risk actions when an environment variable is set. That is not merely incomplete documentation; it overstates the safety properties of the module relative to its actual behavior.

Context-Inappropriate Capability

Medium
Confidence
84% confidence
Finding
No manifest is available, so there is no stated purpose that would justify a generic shell-script dispatcher. The code resolves an action name to an .sh file and executes it via /bin/bash, which is a materially powerful capability beyond what can be inferred from the file's minimal labeling alone.

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.

Static analysis

No suspicious patterns detected.