Back to skill

Security audit

OC Guard

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent OpenClaw configuration guard, but it handles high-impact config and credentials with under-scoped external AI use and unsafe local artifact storage.

Review before installing. Use --proposal with a reviewed local JSON file instead of putting secrets in natural-language requirements, avoid embedding credentials in parent-object changes, and assume /tmp/oc-guard-* files and receipts may contain sensitive config until the redaction and temp-file handling are fixed. Apply should be run only by someone authorized to change OpenClaw agent, tool, plugin, channel, and gateway behavior.

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

T09 · Insecure Skill Coding Practices

Error
Location
` or the corresponding apply command. 3. The path `/channels/feishu/accounts/example` passes the mutation allowlist. 4. `mask_value()` examines only that path and finds no secret keyword. 5. The complete `value` object, including `appSecret`, is placed in the `applied` report. 6. The secret is printed in the execution receipt. 7. The raw proposal is additionally stored at `/tmp/oc-guard-last-proposal.json`. 8. Anyone with access to captured output or the temporary artifact may recover the credential. The s ...[truncated 1034 chars]:104
Finding
Nested configuration secrets are disclosed in receipts and temporary proposal files<![CDATA[ ## Vulnerability Details **File Location**: `scripts/oc-guard.py:46`, `scripts/oc-guard.py:104-109`, `scripts/oc-guard.py:627-650`, `scripts/oc-guard.py:728-756`, and `scripts/oc-guard.py:758-824` **Vulnerability Type**: Inadequate recursive 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 ``` The masking function is invoked using only the top-level mutation path: ```python def apply_changes(cfg, proposal): changes = proposal.get("changes") if not isinstance(changes, list) or not changes: raise ValueError("proposal.changes must be non-empty list") modified = copy.deepcopy(cfg) applied = [] risks = [] for c in changes: if not isinstance(c, dict): raise ValueError("each change must be object") op = c.get("op", "set") path = c.get("path") if not isinstance(path, str): raise ValueError("change.path must be string") if not path.startswith(ALLOWED_PATH_PREFIXES): raise ValueError(f"path not allowed by policy: {path}") 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}) elif op == "delete": delete_path(modified, path) applied.append({"op": op, "path": path}) else: raise ValueError(f"unsupported op: {op}") risks.append(risk_of_path(path)) return modified, applied, merge_risk(risks) ``` The resulting value is pri ...[truncated 3289 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace path-only masking with recursive redaction of dictionaries and lists. ```python def redact_value(value, key_path=""): if isinstance(value, dict): result = {} for key, child in value.items(): child_path = f"{key_path}/{key}" if SECRET_RE.search(str(key)): result[key] = "****" else: result[key] = redact_value(child, child_path) return result if isinstance(value, list): return [redact_value(item, key_path) for item in value] if SECRET_RE.search(key_path): return "****" return value ``` 2. Apply recursive redaction before adding values to receipt data: ```python shown = redact_value(c["value"], path) ``` 3. Do not persist raw proposals when they may contain credentials. Persist only a redacted copy unless the original is strictly required for execution. 4. If raw persistence is unavoidable: - Store the file in a private, user-owned directory. - Set directory permissions to `0700`. - Set file permissions to `0600`. - Delete the artifact as soon as it is no longer required. - Document its sensitive nature clearly. 5. Extend secret detection to normalize key names and cover common variants such as `authorization`, `credential`, `privateKey`, `clientSecret`, and `accessKey`. 6. Add regression tests for: - A direct secret path. - A parent-object mutation containing `appSecret`. - Nested dictionaries containing `botToken`. - Lists containing credential objects. - Plan and apply receipt output. - Persisted proposal artifacts. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/oc-guard.py:26
Finding
Predictable shared temporary files allow symlink attacks and insecure artifact exposure<![CDATA[ ## Vulnerability Details **File Location**: `scripts/oc-guard.py:26-30`, `scripts/oc-guard.py:66-68`, `scripts/oc-guard.py:84-86`, `scripts/oc-guard.py:596-619`, `scripts/oc-guard.py:735-742`, and `scripts/oc-guard.py:773-774` **Vulnerability Type**: Unsafe predictable temporary files **Risk Level**: Medium ### Vulnerable Code The application uses globally predictable paths in the shared `/tmp` directory: ```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") ``` The log path is opened without symlink or ownership validation: ```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") ``` The error path is overwritten directly: ```python def fail(msg: str, code: int = 1, status: str = "失败") -> None: ERROR_PATH.write_text(msg + "\n", encoding="utf-8") log(f"ERROR: {msg}") raise GuardError(msg, status=status, code=code) ``` Diagnostic output is also written to a fixed name: ```python OPENCODE_DEBUG_PATH.write_text( json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) ``` Plan and proposal artifacts are written in the same manner: ```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" ) ``` ```python dump_json(LAST_PROPOSAL_PATH, proposal) ``` ### Technical Analysis The application writes multiple artifacts to fixed, publicly predictable names under `/tmp`. The write operations use normal ...[truncated 2891 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace shared fixed paths with a private per-user runtime directory, preferably under: - `$XDG_RUNTIME_DIR/oc-guard`, when available; or - A directory created with `tempfile.mkdtemp()` and mode `0700`. 2. Create files with restrictive permissions and symlink protection: - Use `os.open()` with `O_CREAT | O_EXCL | O_WRONLY`. - Add `O_NOFOLLOW` where supported. - Set the file mode to `0600`. - Verify with `fstat()` that the opened object is a regular file. 3. For files that must retain a stable “last result” name: - Write to a securely created temporary file in the same private directory. - Flush and optionally `fsync()` the file. - Atomically replace the destination with `os.replace()`. - Verify that the destination directory is owned by the current user and is not group- or world-writable. 4. Configure the log using a private file with mode `0600`, or use a platform logging facility that provides ownership and rotation guarantees. 5. Redact secrets before writing proposal or diagnostic artifacts, independently of filesystem permissions. 6. Remove stale runtime files after use and avoid documenting shared `/tmp` files as long-lived diagnostic storage. 7. Add security tests that: - Pre-create every runtime path as a symbolic link and verify that the operation fails safely. - Confirm all created files have mode `0600`. - Confirm the runtime directory has mode `0700`. - Confirm files cannot be read by an unrelated local account. ]]>
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
Line L212 instructs agents to use bilingual phrasing wherever the repository already does so, effectively imposing a language/locale behavior by default. The file does not indicate this is optional, user-selectable, or required for a documented region-specific compliance reason, which matches the language-policy violation criterion.

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 invokes external commands and implies file-changing behavior, but it does not declare any explicit tool scope such as allowed tools or permissions. This creates an authorization gap where an agent may use shell, file read/write, or environment access more broadly than intended, increasing the chance of unintended command execution or config modification.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The instruction `return `【模型说明-未执行】`` forces a specific language/locale in the skill's natural-language behavior. The file does not offer the user a language choice or explain a justified region-specific requirement, so this is a language policy violation.

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.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The skill sends natural-language requirements to an external AI tool to generate configuration changes that may later be applied to live OpenClaw configuration. Even though downstream path allowlisting and validation exist, this creates an indirect prompt-injection and unsafe-automation boundary where untrusted user text can influence security-sensitive config proposals, including channels, bindings, models, plugins, and tools.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The prompt text instructing the proposal generator is written in Chinese and other user-facing strings throughout the script also require or assume Chinese output. There is no opt-in or language-selection mechanism, so the skill effectively forces a specific language/locale.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
User-supplied natural-language requirements are forwarded to an external subprocess without any explicit notice, consent, or boundary warning in the workflow. In a security-sensitive config-management skill, that increases the risk of unintended disclosure of internal architecture, tokens, account names, or operational details embedded in user requests.

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
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: '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
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.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
After applying config, the skill performs live canary executions against agents, which can trigger real model/tool behavior under the new configuration. In this context, agent configs may enable external network access, tool use, or integration side effects, so an apply operation can cause unintended outbound actions or data exposure beyond mere validation.

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
86% confidence
Finding
The canary message uses a Chinese instruction (`请仅回复`) to direct agent behavior, reflecting a fixed language assumption. Because the script provides no language choice or locale opt-in, this is a natural-language policy concern.

Natural-Language Policy Violations

Low
Confidence
75% confidence
Finding
This manifest contains user-facing natural language in Chinese, including the post-check expectation on L15, but provides no indication that language selection is optional or that the skill is intentionally limited to a Chinese-speaking environment. Under the policy, locale or language constraints should be documented or offer user opt-in to avoid implicit language forcing.

Static analysis

No suspicious patterns detected.