Back to skill

Security audit

Oc Guard Skill

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly purpose-aligned, but it needs Review because it can change high-impact OpenClaw settings while storing sensitive proposal data in unsafe temporary files.

Review before installing on any multi-user or production host. Use only with trusted proposal files and avoid placing real tokens or app secrets in proposals until redaction and private-file handling are fixed. Confirm exact diffs before apply, especially changes under /tools, /plugins, /commands, /channels, /models, and /agents.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/oc-guard.py:105
Finding
Nested Secrets Are Exposed in Receipts and Predictable Temporary Artifacts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/oc-guard.py:46, 105-111, 647-649, 735-742, 774` **Vulnerability Type**: Incomplete secret redaction and plaintext sensitive-data storage **Risk Level**: High ### Vulnerable Code ```python SECRET_RE = re.compile(r"(secret|token|apikey|api_key|password)", re.IGNORECASE) ``` ```python def mask_value(path: str, value): if SECRET_RE.search(path): s = str(value) if len(s) <= 8: return "****" return s[:4] + "****" + s[-4:] return value ``` ```python if op == "set": if "value" not in c: raise ValueError(f"missing value for set: {path}") set_path(modified, path, c["value"]) shown = mask_value(path, c["value"]) applied.append({"op": op, "path": path, "value": shown}) ``` ```python LAST_PROPOSAL_PATH.write_text( json.dumps(proposal, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) report = { "intent": proposal.get("intent"), "risk": risk, "changes": applied, "proposalFile": str(LAST_PROPOSAL_PATH), } LAST_PLAN_PATH.write_text( json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) ``` ```python dump_json(LAST_PROPOSAL_PATH, proposal) ``` ### Technical Analysis Secret masking is based exclusively on the mutation path. It does not recursively inspect dictionaries or lists stored as the mutation value. A proposal can therefore set a broad path such as `/channels/feishu` while embedding `appSecret`, `token`, or other credentials below that path. Because the parent path does not contain a secret-related keyword, `mask_value()` returns the entire object unchanged. Even when a path is recognized as sensitive, the implementation preserves the first and last four characters of the credential. This still discloses credential material and conflicts with the project's stated guarantee that secrets are never exposed in receipts. The complete proposal is also written without redaction ...[truncated 1326 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace path-only masking with recursive redaction of dictionaries and lists. 2. Evaluate each nested key against a comprehensive, normalized sensitive-key policy. 3. Replace secret values completely with a fixed marker such as `[REDACTED]`; do not preserve prefixes or suffixes. 4. Avoid persisting raw proposals that may contain credentials. 5. If persistence is operationally necessary, use a user-owned directory with mode `0700` and create files with mode `0600`. 6. Generate a separately sanitized proposal for receipts and diagnostics. 7. Add tests for nested secrets, mixed-case keys, broad parent-path mutations, lists containing credentials, and short secret values. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/oc-guard.py:26
Finding
Predictable Shared Temporary Files Permit Symlink-Based File Clobbering<![CDATA[ ## Vulnerability Details **File Location**: `scripts/oc-guard.py:26-30, 65-69, 84-87, 596-620, 735-742, 874-876` **Vulnerability Type**: Unsafe temporary-file handling and symbolic-link following **Risk Level**: High ### Vulnerable Code ```python LOG_PATH = Path("/tmp/openclaw-config-guard.log") ERROR_PATH = Path("/tmp/openclaw-config-guard-last-error.log") LAST_PROPOSAL_PATH = Path("/tmp/oc-guard-last-proposal.json") LAST_PLAN_PATH = Path("/tmp/oc-guard-last-plan.json") OPENCODE_DEBUG_PATH = Path("/tmp/oc-guard-last-opencode-output.txt") ``` ```python def log(msg: str) -> None: line = f"[{dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}" LOG_PATH.parent.mkdir(parents=True, exist_ok=True) with LOG_PATH.open("a", encoding="utf-8") as f: f.write(line + "\n") ``` ```python def fail(msg: str, code: int = 1, status: str = "failure") -> None: ERROR_PATH.write_text(msg + "\n", encoding="utf-8") log(f"ERROR: {msg}") raise GuardError(msg, status=status, code=code) ``` ```python OPENCODE_DEBUG_PATH.write_text( json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) ``` ```python LAST_PROPOSAL_PATH.write_text( json.dumps(proposal, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) LAST_PLAN_PATH.write_text( json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) ``` ### Technical Analysis The CLI stores logs, errors, generated proposals, plans, and raw model output at globally predictable names in `/tmp`. The write operations use ordinary `open()`, `Path.write_text()`, and JSON file writes, all of which follow symbolic links. The implementation does not use exclusive creation, `O_NOFOLLOW`, ownership validation, regular-file validation, or a private per-user runtime directory. A local attacker can pre-create one of the paths as a symbolic link. When a more privileged victim invokes the CLI, the process can truncate or append to the link targe ...[truncated 1064 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store runtime artifacts under `$XDG_RUNTIME_DIR/oc-guard` or a private directory inside `OPENCLAW_HOME`. 2. Create the containing directory with mode `0700` and verify that it is owned by the invoking user. 3. Create files atomically with `os.open()` using `O_CREAT | O_EXCL | O_NOFOLLOW` and mode `0600`. 4. Use unique per-run filenames containing cryptographically random components. 5. Before reopening a file, use `lstat()` to reject symbolic links and verify ownership and regular-file type. 6. Write plans through a temporary file in the same private directory and atomically replace the destination. 7. Do not retain raw model output or unredacted proposals longer than necessary. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/oc-guard.py:525
Finding
Command-Execution Confirmation Policy Can Be Disabled Without Explicit Confirmation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/oc-guard.py:61, 437-444, 525-545, 768-769` **Vulnerability Type**: Security-control downgrade caused by incomplete risk classification **Risk Level**: High ### Vulnerable Code ```python ALLOWED_TOOLS_EXEC_ASK = {"off", "on-miss", "always"} ``` ```python def check_enum_constraints(cfg): tools = cfg.get("tools", {}) if isinstance(cfg.get("tools", {}), dict) else {} exec_cfg = tools.get("exec", {}) if isinstance(tools.get("exec", {}), dict) else {} ask = exec_cfg.get("ask") if ask is not None and ask not in ALLOWED_TOOLS_EXEC_ASK: allowed = ", ".join(sorted(ALLOWED_TOOLS_EXEC_ASK)) raise ValueError(f"tools.exec.ask must be one of: {allowed}; got: {ask}") ``` ```python def risk_of_path(path: str) -> str: high_prefixes = ( "/channels", "/bindings", "/models/providers", "/agents/defaults/model", "/agents/list", "/agents/by-id", ) medium_prefixes = ( "/messages", "/commands", "/gateway", "/plugins", "/tools", ) if path.startswith(high_prefixes): return "high" if path.startswith(medium_prefixes): return "medium" return "low" ``` ```python if risk == "high" and not confirm: fail("High-risk changes require --confirm", status="blocked") ``` ### Technical Analysis The guard allows `tools.exec.ask` to have the value `"off"`, which can disable command-execution prompts. However, every mutation under `/tools` is classified only as medium risk. The apply workflow requires `--confirm` only when the merged risk is high. As a result, a proposal that sets `/tools/exec/ask` to `"off"` can pass custom validation and be applied without the explicit confirmation required for high-risk changes. The risk model considers only the path prefix and does not compare the old and new values or determine whether the mutation weakens an existing security contro ...[truncated 1489 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Classify `/tools/exec/ask = off` as high or critical risk. 2. Determine risk from both the path and the direction of the value change. 3. Treat reductions in authentication, approval, sandboxing, allowlists, or execution prompts as high-risk changes. 4. Require `--confirm` for all apply operations, or at minimum for `/tools`, `/commands`, `/plugins`, and gateway security settings. 5. Consider rejecting `"off"` unless the caller supplies a separate, clearly named unsafe-mode option. 6. Bind confirmation to a digest of a previously reviewed plan so a different proposal cannot be substituted after approval. 7. Add regression tests confirming that security-control downgrades are blocked without explicit confirmation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/oc-guard.py:674
Finding
Post-Apply Canary Accepts Arbitrary Nonempty Output as Success<![CDATA[ ## Vulnerability Details **File Location**: `scripts/oc-guard.py:674-714` **Vulnerability Type**: Insufficient health-check response validation **Risk Level**: Medium ### Vulnerable Code ```python def run_canary_for_agent(agent_id): probe = f"Please reply only with: OC_GUARD_CANARY_{agent_id}" proc = run( [ str(OPENCLAW_BIN), "agent", "--agent", agent_id, "--message", probe, "--json", ], timeout=120, check=False, ) out = ((proc.stdout or "") + (proc.stderr or "")).strip() if proc.returncode != 0: return False, f"agent={agent_id} command failed: {out[:300]}" text = extract_json_object(out) if not text: return False, f"agent={agent_id} returned non-json output" data = json.loads(text) if data.get("status") != "ok": return False, f"agent={agent_id} status != ok" payloads = data.get("result", {}).get("payloads", []) if not isinstance(payloads, list) or not payloads: return False, f"agent={agent_id} empty payloads" has_content = any( isinstance(item, dict) and ( (isinstance(item.get("text"), str) and item.get("text").strip()) or (isinstance(item.get("mediaUrl"), str) and item.get("mediaUrl").strip()) ) for item in payloads ) if not has_content: return False, f"agent={agent_id} payloads have no content" return True, f"agent={agent_id} canary ok" ``` ### Technical Analysis The canary prompt requests a specific token, but the response validator never checks whether that token was returned. It considers the canary successful when any payload contains nonempty text or a nonempty media URL. Consequently, an error message, unrelated model response, prompt-injected response, or media-only payload is accepted as proof of health. This undermines the rollback control because a broken or misrouted config ...[truncated 1092 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require exact normalized equality with `OC_GUARD_CANARY_<agent_id>`. 2. Reject media-only payloads and responses containing additional or unrelated text. 3. Validate the responding agent's identity and routing metadata when OpenClaw exposes those fields. 4. Use a cryptographically random nonce in each canary to prevent replay or cached responses. 5. Verify that the nonce appears exactly once in the expected text field. 6. Add negative tests for arbitrary text, error messages, media URLs, wrong-agent responses, stale tokens, and prefixed or suffixed tokens. 7. Trigger rollback whenever the exact canary contract is not satisfied. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (18)

Env Variable Harvesting

High
Category
Data Exfiltration
Content
temp_home = Path(td)
        temp_cfg = temp_home / "openclaw.json"
        dump_json(temp_cfg, cfg)
        env = os.environ.copy()
        env["OPENCLAW_CONFIG_PATH"] = str(temp_cfg)
        proc = run([str(OPENCLAW_BIN), "config", "validate", "--json"], timeout=30, check=False, env=env)
        out = ((proc.stdout or "") + (proc.stderr or "")).strip()
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The instruction "Use bilingual phrasing where repository already does so" imposes a language/locale requirement on agent behavior. The policy allows locale constraints only when users are given a choice or when the constraint is clearly justified as region-specific, which is not stated here.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
chmod +x scripts/oc-guard
mkdir -p ~/.local/bin
ln -sf "$PWD/scripts/oc-guard" ~/.local/bin/oc-guard
```
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.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises operational commands that invoke local binaries and imply file and shell access, but it does not declare any explicit tool scope such as permissions or allowed-tools. That mismatch weakens policy enforcement and reviewability, increasing the risk that an agent can perform filesystem or command execution beyond what a caller expects.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The skill requires a fixed response format, including a Chinese fallback string and an execution receipt-first requirement, without user opt-in. While not directly enabling code execution, it can override user preferences and manipulate agent output behavior, which may obscure failures or reduce transparency in multilingual contexts.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The file embeds Chinese-language markers and consistently emits Chinese or bilingual strings in prompts, errors, receipts, and canary messages, without offering the user a language choice. That creates a locale/language policy issue because the skill enforces a specific language behavior rather than allowing opt-in or selection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run(cmd, timeout=60, check=True, env=None):
    return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=check, env=env)


def load_json(path: Path):
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
The code reads a secret from the OCGUARD_RECEIPT_SECRET environment variable and falls back to creating and storing a secret on disk, but there is no confirmation prompt or user-facing disclosure at the point of access. In this file, the command output and logs describe config operations, yet they do not warn that credential-like material may be read from environment state or persisted locally.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The skill sends natural-language requirements to an external AI tool and then applies the returned JSON proposal to live configuration after only structural validation. In this context, the AI-generated changes can alter channels, bindings, models, tools, and gateway behavior, so prompt manipulation or unsafe proposals may lead to misconfiguration, expanded access, or service disruption.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
Natural-language requirements are forwarded to the external opencode subprocess without an explicit warning or consent mechanism. In a security-sensitive config-management tool, this matters because operators may unintentionally expose internal infrastructure details, credentials, or business context to a separate AI toolchain.

Tainted flow: 'payload' from os.environ.get (line 591, credential/environment) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
"stdout": stdout,
            "stderr": stderr,
        }
        OPENCODE_DEBUG_PATH.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
Confidence
81% confidence
Finding
When opencode returns invalid output, the script writes raw stdout/stderr into a world-accessible temporary path under /tmp. Since the external AI tool may echo prompts, requirements, config-derived content, or secrets, this creates a local information-disclosure risk to other users/processes on the same host.

Tainted flow: 'payload' from os.environ.get (line 609, credential/environment) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
"extracted_json": text,
            "parse_error": str(e),
        }
        OPENCODE_DEBUG_PATH.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
Confidence
82% confidence
Finding
On JSON parse failure, the script persists raw opencode stdout/stderr plus extracted JSON and parse errors to /tmp. Because this data may contain secrets, prompts, or internal configuration details, it can leak sensitive information to local adversaries or other processes able to read temporary files.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The stated purpose is config planning/apply with receipts, but the code additionally sends probe messages to configured agents and validates their responses after apply. This operational agent-execution capability goes beyond merely planning and applying configuration and is not clearly disclosed in the manifest description.

Tainted flow: 'backup_file' from os.environ.get (line 773, credential/environment) → shutil.copy2 (file write)

Medium
Category
Data Flow
Content
ts = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_file = BACKUP_DIR / f"openclaw.json.{ts}.bak"
    dump_json(LAST_PROPOSAL_PATH, proposal)
    shutil.copy2(config_file, backup_file)
    dump_json(config_file, modified)

    log(f"Backup created: {backup_file}")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'backup_file' from os.environ.get (line 773, credential/environment) → shutil.copy2 (file write)

Medium
Category
Data Flow
Content
restarted = run([str(OPENCLAW_BIN), "gateway", "restart"], timeout=60, check=False)
    if restarted.returncode != 0:
        log("Gateway restart failed, rolling back")
        shutil.copy2(backup_file, config_file)
        run([str(OPENCLAW_BIN), "gateway", "restart"], timeout=60, check=False)
        fail("restart failed after apply; rolled back")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'backup_file' from os.environ.get (line 773, credential/environment) → shutil.copy2 (file write)

Medium
Category
Data Flow
Content
restarted = run([str(OPENCLAW_BIN), "gateway", "restart"], timeout=60, check=False)
    if restarted.returncode != 0:
        log("Gateway restart failed, rolling back")
        shutil.copy2(backup_file, config_file)
        run([str(OPENCLAW_BIN), "gateway", "restart"], timeout=60, check=False)
        fail("restart failed after apply; rolled back")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'backup_file' from os.environ.get (line 773, credential/environment) → shutil.copy2 (file write)

Medium
Category
Data Flow
Content
restarted = run([str(OPENCLAW_BIN), "gateway", "restart"], timeout=60, check=False)
    if restarted.returncode != 0:
        log("Gateway restart failed, rolling back")
        shutil.copy2(backup_file, config_file)
        run([str(OPENCLAW_BIN), "gateway", "restart"], timeout=60, check=False)
        fail("restart failed after apply; rolled back")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
This manifest contains natural-language strings in both Chinese and English, such as the intent on L02 and the post-check message on L15. Because the file provides no indication that language choice is user-selectable or that the locale is intentionally constrained, it may violate organizational language/locale policy expectations.

Static analysis

No suspicious patterns detected.