T09 · Insecure Skill Coding Practices
- Location
scripts/oc-guard.py:105- Finding
Nested Secrets Are Exposed in Receipts and Predictable Temporary Artifacts
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/oc-guard.py:105Nested Secrets Are Exposed in Receipts and Predictable Temporary Artifacts
scripts/oc-guard.py:26Predictable Shared Temporary Files Permit Symlink-Based File Clobbering
scripts/oc-guard.py:525Command-Execution Confirmation Policy Can Be Disabled Without Explicit Confirmation
scripts/oc-guard.py:674Post-Apply Canary Accepts Arbitrary Nonempty Output as Success
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.
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()
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.
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.
chmod +x scripts/oc-guard
mkdir -p ~/.local/bin
ln -sf "$PWD/scripts/oc-guard" ~/.local/bin/oc-guard
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.
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.
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 calls execute external commands. Without careful input validation, this enables command injection.
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):
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.
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.
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.
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.
"stdout": stdout,
"stderr": stderr,
}
OPENCODE_DEBUG_PATH.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
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.
"extracted_json": text,
"parse_error": str(e),
}
OPENCODE_DEBUG_PATH.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
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.
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
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}")
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
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")
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
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")
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
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")
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.