Back to skill

Security audit

OpenClaw Docs

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent OpenClaw documentation helper, but its scripts accept overly broad URL and file inputs that can reach non-doc services, write outside the intended cache, or print local file contents.

Review before installing. Use this only in an environment where shell scripts cannot be driven by untrusted input, avoid passing full URLs or local file paths to its commands, and prefer a fixed docs.openclaw.ai-only version that validates cache paths before writing.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/lib.sh:61
Finding
Unrestricted documentation URL enables server-side request forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib.sh:61-79`, `scripts/lib.sh:99-113`, and `scripts/fetch-doc.sh:9-15` **Vulnerability Type**: Unrestricted outbound request / SSRF **Risk Level**: High ### Vulnerable Code ```bash normalize_doc_url() { local input="${1:-}" if [[ -z "$input" ]]; then echo "Usage: provide a docs path like gateway/configuration or a full docs.openclaw.ai URL" >&2 return 1 fi if [[ "$input" =~ ^https?:// ]]; then local base_no_query query base_no_query="${input%%\?*}" query="" if [[ "$input" == *\?* ]]; then query="?${input#*\?}" fi if [[ "$base_no_query" != *.md ]]; then base_no_query="${base_no_query%/}.md" fi printf '%s%s\n' "$base_no_query" "$query" return fi local path="$input" path="${path#/}" path="${path%.html}" if [[ "$path" != *.md ]]; then path="${path}.md" fi printf '%s/%s\n' "$BASE_URL" "$path" } download_doc() { local url="$1" local dest tmp force_fetch ensure_cache_dirs dest=$(doc_cache_path "$url") mkdir -p "$(dirname "$dest")" force_fetch="${OPENCLAW_DOCS_FORCE_FETCH:-0}" if [[ -s "$dest" && "$force_fetch" != "1" ]]; then printf '%s\n' "$dest" return fi tmp="${dest}.tmp" curl -fsSL "$url" -o "$tmp" mv "$tmp" "$dest" printf '%s\n' "$dest" } ``` The vulnerable functionality is directly exposed by `fetch-doc.sh`: ```bash url=$(normalize_doc_url "$1") cache_path=$(download_doc "$url") echo "# Source: $url" echo "# Cached: $cache_path" echo cat "$cache_path" ``` ### Technical Analysis `normalize_doc_url` accepts every string beginning with `http://` or `https://`. It does not require the exact documented host, `docs.openclaw.ai`, and does not reject loopback, link-local, private-network, or other non-documentation destinations. The resulting URL is passed directly to `curl`. The `-L` option also permits redirects, but the final redirect destination is not validated. Consequently, vali ...[truncated 1674 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Accept only HTTPS URLs whose parsed hostname is exactly `docs.openclaw.ai`. 2. Reject URL user-information, non-default ports, fragments, backslashes, control characters, and ambiguous or encoded path separators. 3. For path-only input, permit only a conservative documentation-path character set and reject `.` and `..` path segments. 4. Disable redirects where they are unnecessary. If redirects are required, validate the destination of every redirect before following it. 5. Apply an explicit curl protocol policy, such as HTTPS-only restrictions, in addition to application-level validation. 6. Reject loopback, private, link-local, and reserved IP destinations after DNS resolution as defense in depth. 7. Keep `OPENCLAW_DOCS_BASE_URL` and `OPENCLAW_DOCS_INDEX_URL` overrides disabled or separately validated in untrusted execution environments. 8. Add negative tests for foreign hosts, deceptive hostnames, alternate ports, URL credentials, encoded traversal, and redirects from the approved host to an unapproved host. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/lib.sh:91
Finding
Untrusted URL path permits writes outside the documentation cache<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib.sh:91-113` **Vulnerability Type**: Path traversal / arbitrary file write **Risk Level**: High ### Vulnerable Code ```bash doc_cache_path() { local url="$1" local without_query rel without_query="${url%%\?*}" rel="${without_query#${BASE_URL}/}" printf '%s\n' "$CACHE_DIR/docs/$rel" } download_doc() { local url="$1" local dest tmp force_fetch ensure_cache_dirs dest=$(doc_cache_path "$url") mkdir -p "$(dirname "$dest")" force_fetch="${OPENCLAW_DOCS_FORCE_FETCH:-0}" if [[ -s "$dest" && "$force_fetch" != "1" ]]; then printf '%s\n' "$dest" return fi tmp="${dest}.tmp" curl -fsSL "$url" -o "$tmp" mv "$tmp" "$dest" printf '%s\n' "$dest" } ``` ### Technical Analysis `doc_cache_path` treats a URL-derived value as a relative filesystem path without canonicalization or containment validation. It does not reject `..` segments, encoded separators, backslashes, or URLs outside `BASE_URL`. For a foreign-host URL, this expansion does not remove a trusted prefix: ```bash rel="${without_query#${BASE_URL}/}" ``` The complete attacker-influenced value therefore becomes part of the destination path. For a URL associated with the configured base, traversal components in its path are still retained. Because the destination is prefixed with `$CACHE_DIR/docs/` but not canonicalized, enough `../` components can escape the cache directory. `mkdir -p`, `curl -o`, and `mv` then create and replace the derived path. The use of a temporary filename does not prevent traversal because both the temporary path and final path are based on the unsafe destination. ### Attack Path 1. An attacker supplies a crafted URL containing traversal components to the documented `fetch-doc.sh` entry point. 2. `normalize_doc_url` accepts the URL because arbitrary HTTP and HTTPS hosts and paths are permitted. 3. `doc_cache_path` concatenates the URL-derived path with `$CACHE_DIR/docs/`. 4. Filesyste ...[truncated 1339 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate the URL before deriving any filesystem path. Only allow the exact approved scheme and host. 2. Parse and decode the URL path using a well-defined URL parser rather than shell prefix substitution. 3. Reject empty segments where ambiguous, `.` and `..` components, encoded traversal, control characters, backslashes, and encoded path separators. 4. Canonicalize both the cache root and candidate parent path, then verify that the candidate remains strictly beneath the canonical cache root. 5. Prefer mapping approved URLs to hash-based cache filenames rather than reproducing remote paths directly. 6. Create temporary files with `mktemp` inside a trusted cache directory and move them only to a validated destination. 7. Refuse to follow symbolic links in cache path components where supported, and ensure the cache directory is not writable by unrelated users. 8. Add tests demonstrating that traversal strings and foreign-host URLs cannot create files outside `$CACHE_DIR/docs`. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/lib.sh:134
Finding
Snapshot resolver accepts arbitrary local files and may disclose their contents<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib.sh:134-185` and `scripts/track-changes.sh:21-50` **Vulnerability Type**: Arbitrary local file read **Risk Level**: Medium ### Vulnerable Code ```bash resolve_snapshot() { local ref="${1:-}" ensure_cache_dirs if [[ -z "$ref" ]]; then return 1 fi if [[ -f "$ref" ]]; then printf '%s\n' "$ref" return fi if [[ -f "$(snapshot_path "$ref")" ]]; then snapshot_path "$ref" return fi local match match=$(find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name "${ref}*.tsv" | sort | head -n1 || true) if [[ -n "$match" ]]; then printf '%s\n' "$match" return fi return 1 } compare_snapshots() { local older="$1" local newer="$2" local a b added removed a=$(mktemp) b=$(mktemp) trap 'rm -f "$a" "$b" "$added" "$removed"' RETURN sort "$older" > "$a" sort "$newer" > "$b" added=$(mktemp) removed=$(mktemp) comm -13 "$a" "$b" > "$added" comm -23 "$a" "$b" > "$removed" echo "Comparing $(basename "$older") -> $(basename "$newer")" echo echo "Added pages: $(wc -l < "$added" | tr -d ' ')" if [[ -s "$added" ]]; then cut -f1,2 "$added" else echo "(none)" fi echo echo "Removed pages: $(wc -l < "$removed" | tr -d ' ')" if [[ -s "$removed" ]]; then cut -f1,2 "$removed" else echo "(none)" fi } ``` The arbitrary path is reachable through the `since` command: ```bash ref="${2:-}" older="" if older=$(resolve_snapshot "$ref" 2>/dev/null); then : else cutoff=$(date -u -d "$ref" +%s 2>/dev/null || true) if [[ -n "$cutoff" ]]; then older=$(find "$CACHE_DIR/snapshots" -maxdepth 1 -type f -name '*.tsv' -printf '%T@ %p\n' | sort -n | awk -v t="$cutoff" '$1 >= t {print substr($0, index($0,$2)); exit}') fi fi ... compare_snapshots "$older" "$latest" ``` ### Technical Analysis The documented input is a snapshot prefix or date, but `resolve_snapshot` first accepts any existing regular file: ```bash if [[ -f "$r ...[truncated 1700 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove support for resolving an arbitrary existing path in `resolve_snapshot`. 2. Accept only a conservative snapshot identifier format, such as the timestamp format generated by `snapshot_now`. 3. Reject path separators, `..`, shell metacharacters, and identifiers beginning with `-`. 4. Construct the path exclusively beneath `$CACHE_DIR/snapshots`. 5. Canonicalize the candidate and snapshot root and verify strict directory containment before opening the file. 6. Require the `.tsv` extension and validate that the file matches the expected snapshot structure before comparison. 7. Use `find --` or otherwise ensure user input cannot be interpreted as an option or unsafe pattern. 8. Add tests confirming that absolute paths, relative paths, traversal strings, and files outside the snapshot directory are rejected. ]]>
Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (1)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill instructs the agent to execute multiple shell scripts, but it does not declare any explicit tool scope such as allowed tools or permissions. This creates an authorization ambiguity where a consumer may permit shell execution more broadly than intended, increasing the chance of unintended command execution against the local environment or network.

Static analysis

No suspicious patterns detected.