Back to skill

Security audit

Openclaw Daily Ops

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent operations purpose, but it can automatically truncate session files and send usage metadata externally with weak safeguards.

Review before installing. Use dry-run first, run under an unprivileged account, restrict config and session metadata permissions, validate that sessionFile paths stay inside the intended sessions directory, and avoid enabling nightly cron until you are comfortable with permanent session truncation and Discord reporting of usage metadata.

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/zombie_killer.py:50
Finding

Unrestricted File Truncation Through Untrusted Session Metadata

Content
View full analysis

Vulnerability Details

File Location: scripts/zombie_killer.py:50-52, 63-72, 84-86
Vulnerability Type: Arbitrary file truncation caused by insufficient path validation
Risk Level: High

Vulnerable Code

python
for sk, meta in sessions_meta.items():
    jsonl_path = meta.get("sessionFile", "")
    if not jsonl_path or not os.path.exists(jsonl_path):
        continue

    updated_at_ms = meta.get("updatedAt", 0)
    if not updated_at_ms:
        continue

    updated_at = datetime.fromtimestamp(updated_at_ms / 1000, tz=timezone.utc)
    age_hours = (now - updated_at).total_seconds() / 3600
    file_size = os.path.getsize(jsonl_path)

    if age_hours <= min_age_hours or file_size < min_size_bytes:
        skipped += 1
        continue

    # ...

    if args.dry_run:
        print(f"[zombie_killer] DRY RUN — would reset: {sk[:40]} ({age_hours:.1f}h old, {file_size/1_000_000:.1f}MB, {message_count} msgs)")
    else:
        try:
            open(jsonl_path, "w").close()

Technical Analysis

The sessionFile path is read directly from sessions.json and used as the target of a destructive write operation. The script only checks that the path exists. It does not verify that:

  • The canonical path is contained within the configured sessions_dir.
  • The target is a regular session JSONL file.
  • The target is not a symbolic link.
  • The path still refers to the same file immediately before truncation.
  • The metadata file and target are owned by an expected user.

Opening a path with mode "w" immediately truncates it. Consequently, a malicious or corrupted sessions.json entry can point to any writable file visible to the account running the scheduled job. A symbolic link inside or outside the sessions directory can produce the same result.

The age and size conditions do not establish that the target is a legitimate session file. An attacker can supply an old `u ...[truncated 1337 chars]

Remediation
View remediation

Remediation Suggestions

  • Resolve both sessions_dir and each candidate path with os.path.realpath() and enforce containment with os.path.commonpath().
  • Reject paths outside the canonical sessions directory.
  • Require an expected .jsonl suffix and verify that the target is a regular file.
  • Reject symbolic links with os.path.islink() and use descriptor-based opening with O_NOFOLLOW where supported.
  • Revalidate file identity immediately before any destructive operation to reduce time-of-check/time-of-use exposure.
  • Prefer atomically renaming the file into a restricted quarantine directory instead of truncating it.
  • Retain recoverable backups for a defined period before permanent deletion.
  • Validate the ownership and permissions of sessions.json, sessions_dir, and each target.
  • Run the scheduled task under a dedicated, unprivileged account with write access limited to the intended session and state directories.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/cost_report.py:185
Finding

Operational Metadata Can Be Sent to an Unrestricted Webhook Destination

Content
View full analysis

Vulnerability Details

File Location: scripts/cost_report.py:185-201, 251-257
Vulnerability Type: Missing outbound destination validation
Risk Level: Medium

Vulnerable Code

python
def post_discord(webhook_url, message):
    payload = json.dumps({"content": message[:2000]}).encode()
    req = Request(
        webhook_url,
        data=payload,
        headers={"Content-Type": "application/json", "User-Agent": "OpenClaw-DailyOps/1.0"},
        method="POST",
    )
    try:
        resp = urlopen(req, timeout=10)
        return True
    except Exception as e:
        code = getattr(e, "code", None)
        if code == 204:
            return True
        print(f"[cost_report] Discord post failed: {e}", file=sys.stderr)
        return False

# ...

webhook = cfg.get("discord_webhook", "")
if not webhook or "YOUR_WEBHOOK" in webhook:
    print("[cost_report] No Discord webhook configured — printing report instead:")
    print(report)
    return

if post_discord(webhook, report):
    print(f"[cost_report] Report posted — ${total_cost:.2f} today")

Technical Analysis

The feature intentionally sends a cost report to Discord, so outbound transmission is necessary for the declared reporting functionality. However, the implementation does not verify that the configured URL actually belongs to Discord.

Any destination accepted by urllib.request.urlopen() can be supplied through config.json. There is no enforcement of HTTPS, no hostname or webhook-path allowlist, and no explicit prevention of cross-host redirects. The payload can contain:

  • Session or channel display names.
  • Per-session token consumption and API costs.
  • Daily totals and historical trends.
  • A configured Discord user ID in high-cost alerts.
  • A caller-supplied zombie summary.

This is not covert exfiltration because webhook reporting is documented, but insufficient destination validation me ...[truncated 1403 chars]

Remediation
View remediation

Remediation Suggestions

  • Parse the configured URL and require the https scheme.
  • Allow only explicitly supported Discord webhook hosts and the expected /api/webhooks/ path structure.
  • Reject URLs containing unexpected user information, ports, fragments, or malformed webhook paths.
  • Disable redirects or validate every redirect destination against the same host and scheme allowlist.
  • Store config.json with restrictive permissions because the webhook URL itself contains a secret token.
  • Document all fields transmitted externally and remove or pseudonymize session labels when detailed identification is unnecessary.
  • Provide an option to report only aggregate cost and token totals.
  • Apply outbound firewall rules so the dedicated job account can communicate only with approved Discord endpoints.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/zombie_killer.py:63
Finding

Session Reset Safety and Logging Guarantees Are Not Enforced

Content
View full analysis

Vulnerability Details

File Location: scripts/zombie_killer.py:63-72, 84-113
Vulnerability Type: Unsafe destructive operation, race condition, and non-atomic audit logging
Risk Level: Medium

Vulnerable Code

python
updated_at = datetime.fromtimestamp(updated_at_ms / 1000, tz=timezone.utc)
age_hours = (now - updated_at).total_seconds() / 3600
file_size = os.path.getsize(jsonl_path)

if age_hours <= min_age_hours or file_size < min_size_bytes:
    skipped += 1
    continue

# Count messages for the log
try:
    with open(jsonl_path) as f:
        message_count = sum(1 for _ in f)
except Exception:
    message_count = -1

# ...

else:
    try:
        open(jsonl_path, "w").close()
        print(f"[zombie_killer] Reset: {sk[:40]} ({age_hours:.1f}h old, {file_size/1_000_000:.1f}MB freed)")
    except Exception as e:
        print(f"[zombie_killer] Failed to reset {sk}: {e}", file=sys.stderr)
        reset_log.pop()

# Save log
if not args.dry_run and reset_log:
    log_path = os.path.join(workspace_dir, "state", "session-reset-log.json")
    os.makedirs(os.path.dirname(log_path), exist_ok=True)
    try:
        with open(log_path) as f:
            existing = json.load(f)
    except Exception:
        existing = []

    existing.extend(reset_log)
    existing = existing[-500:]

    with open(log_path, "w") as f:
        json.dump(existing, f, indent=2)

The documentation makes stronger guarantees than the implementation provides:

markdown
**Q: Will this break my active sessions?**
No. The zombie killer only resets sessions older than 24h AND larger than 1MB. Active sessions are never touched.

**Q: What does "reset" mean?**
It truncates the session JSONL file to empty. OpenClaw recreates it fresh on next use. All logs are saved to `session-reset-log.json` before wiping.

Technical Analysis

The script treats sessions.json met ...[truncated 1956 chars]

Remediation
View remediation

Remediation Suggestions

  • Coordinate resets through an OpenClaw-supported session-management API where available rather than modifying live files directly.
  • Acquire an application-compatible exclusive lock before checking and resetting a session.
  • Verify both metadata activity and the target file's current modification time.
  • Recheck age, size, canonical path, and file identity after obtaining the lock.
  • Atomically move stale sessions to a restricted quarantine directory instead of truncating them.
  • Retain quarantined content for a configurable recovery period.
  • Write and flush an intent record before the destructive operation, then atomically update it with success or failure afterward.
  • Write logs through a temporary file followed by os.replace() to avoid partial JSON files.
  • Handle log persistence failures explicitly and do not proceed with destruction if a required audit record cannot be secured.
  • Revise the documentation to state that age is inferred from metadata and that the current log is not a session-content backup.
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (10)

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding

The code clearly implements the cost-reporting portion of the description: it reads session logs, computes daily per-session and total spend, preserves history, renders a 7-day trend, and posts to Discord. However, the description also claims 'session hygiene' and specifically says it 'wipes zombie sessions >24h old.' This code does not inspect session age, delete sessions, modify session files, or perform cleanup. The only related behavior is accepting a --zombie-summary argument to append text from some other script. That is a material description/behavior mismatch because one of the declared core functions is absent from the supplied code chunk.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The declared description presents a combined skill whose primary functions include daily cost reporting and spend trend analysis, with session cleanup as one component. The supplied code chunk implements only the cleanup portion. It reads session metadata from sessions.json, checks file age and size, truncates qualifying session files, records a reset log, and prints a summary for another script to consume. There is no logic for API cost aggregation, spend reporting, 7-day trends, personal-info handling, cron scheduling, or Discord integration. Because major declared capabilities are absent from the actual code, the description does not accurately represent this code chunk.

Content

No source excerpt is available for this finding.

Instruction Override

High
Category
Prompt Injection
Confidence
70% confidence
Finding

This pattern attempts to override system instructions or ignore safety constraints. Without LLM analysis, manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 53)May include surrounding context.

md
2. Copy the webhook URL

**How to get your Discord user ID:**
1. Enable Developer Mode in Discord (Settings → Advanced → Developer Mode)
2. Right-click your username → Copy User ID

**Channel names** (optional): Map your OpenClaw channel session keys to human-readable names for the report. Find session keys in `~/.openclaw/agents/main/sessions/sessions.json`.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The README explicitly promotes automatic wiping/reset of stale sessions older than 24 hours, but it does not clearly warn that this may permanently delete conversation state or other session data. Because this is an operational automation skill intended to run nightly via cron, a user could enable destructive behavior without fully understanding the irreversibility, leading to accidental data loss or service disruption.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill instructs an agent to read session files, write logs and truncate session data, and post results to a Discord webhook, but it does not declare any explicit tool scope or allowed-tools constraints. That increases the blast radius if the skill is misused or interpreted broadly, because file and network actions could occur without least-privilege guardrails.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The introductory description advertises cost reporting and hygiene but does not prominently warn that the skill will irreversibly truncate old session files. Because the action is destructive and scheduled via cron, users may enable it without appreciating the data-loss risk, making accidental loss of conversation history more likely.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
85% 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.

Content

Scanner excerpt · SKILL.md (reported line 79)May include surrounding context.

Or run it as a system cron:

bash
# Add to crontab -e
0 21 * * * python3 /path/to/openclaw-daily-ops/scripts/cost_report.py --config /path/to/config.json >> /path/to/cost_report.log 2>&1

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The cron payload tells the agent to post outputs to a Discord webhook without an explicit warning that session-derived metadata will leave the local environment. Even if the author intends to exclude personal info, session names, costs, token counts, or operational details can still be sensitive and become externally disclosed.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The script claims it 'never touches active sessions,' but enforcement is based only on timestamp age and file size from metadata, with no lock check, PID ownership check, or verification that the session file is not currently in use. In a nightly cron context, stale or skewed metadata could cause truncation of a session that is still active, resulting in data loss or interruption of ongoing agent work.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

This code performs an irreversible file write by opening each session file in write mode and immediately truncating it. Although it logs after the reset and the module docstring mentions truncation, there is no user confirmation or explicit runtime warning immediately before the destructive action.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.