Back to skill

Security audit

Agent Budget Governance

Security checks for vulnerabilities and agentic risk

Overview

This skill is a local budget-audit tool, but it overstates its enforcement capabilities and can modify governance files in ways users should review carefully before relying on it.

Review this as an audit/reporting script, not a complete spend-control or privilege-revocation system. Before installing or scheduling it, restrict it to a dedicated agents directory, ensure agents cannot replace BUDGET.json with links or malformed files, add backups or dry-run behavior, and do not rely on it to block spawns unless separate OpenClaw gateway or openclaw.json enforcement is implemented.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (3)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/budget_audit.py:46
Finding

Symlink Following Allows Unauthorized File Modification

Content
View full analysis
dict: """Audit one agent's budget. Returns report dict.""" budget_path = os.path.join(agent_dir, "BUDGET.json") if not os.path.exists(budget_path): return {"agent": agent_name, "status": "no_budget_file"} with open(budget_path) as f: budget = json.load(f) ``` The same path is subsequently opened for writing in both the daily-reset and same-day execution paths: ```python with open(budget_path, "w") as f: json.dump(budget, f, indent=2) ``` ```python with open(budget_path, "w") as f: json.dump(budget, f, indent=2) ``` Agent directories are discovered without validating the contained budget file: ```python agent_dirs = sorted(glob.glob(os.path.join(AGENTS_DIR, "*"))) for agent_dir in agent_dirs: if not os.path.isdir(agent_dir): continue dirname = os.path.basename(agent_dir) if dirname == "governance": continue agent_name = AGENT_MAP.get(dirname, dirname) budget_path = os.path.join(agent_dir, "BUDGET.json") if not os.path.exists(budget_path): continue report = audit_agent(agent_dir, agent_name, today) reports.append(report) ``` ### Technical Analysis The auditor opens `BUDGET.json` through a pathname without rejecting symbolic links or verifying that the resolved file remains inside the expected agent directory. Python's ordinary `open()` follows symbolic links. The script then rewrites the same resolved target after changing budget-related fields. This creates a privilege-boundary problem when an agent can modify its own directory while the audit runs under a more privileged orchestrator or heartbeat account. ...[truncated 1800 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/budget_audit.py:46
Finding

Self-Reported Budget State Bypasses Governance and Demotion Controls

Content
View full analysis
dict: """Audit one agent's budget. Returns report dict.""" budget_path = os.path.join(agent_dir, "BUDGET.json") if not os.path.exists(budget_path): return {"agent": agent_name, "status": "no_budget_file"} with open(budget_path) as f: budget = json.load(f) report = { "agent": agent_name, "dir": os.path.basename(agent_dir), "limit": budget.get("daily_limit_output_tokens", 50000), "used": budget.get("used_output_tokens", 0), "status": budget.get("status", "active"), "consecutive_overbudget": budget.get("consecutive_overbudget_days", 0), "spawns_today": len(budget.get("spawns", [])), "actions": [], } ``` Demotion changes only a value in `BUDGET.json`; it does not revoke the agent's actual spawn authorization: ```python limit = budget.get("daily_limit_output_tokens", 50000) used = budget.get("used_output_tokens", 0) pct = used / limit if limit > 0 else 0 report["pct"] = round(pct * 100, 1) if pct >= EMERGENCY_MULTIPLIER and budget["status"] != "demoted": budget["status"] = "demoted" report["actions"].append(f"EMERGENCY DEMOTION: {report['pct']}% of daily budget") log_event(agent_name, "demotion", { "reason": f"Emergency: {report['pct']}% in single day", "used": used, "limit": limit, }) elif pct >= RED_THRESHOLD and budget["status"] == "active": budget["status"] = "red" report["actions"].append(f"RED: Over budget at {report['pct']}%") log_event(agent_name, "red", ...[truncated 3370 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/budget_audit.py:64
Finding

Malformed Agent Budget Data Can Terminate the Entire Audit

Content
View full analysis
previous_limit # Update consecutive overbudget counter consecutive = budget.get("consecutive_overbudget_days", 0) if was_over: consecutive += 1 log_event(agent_name, "overbudget_day", { "day": budget_day, "used": previous_used, "limit": previous_limit, "pct": round(previous_used / previous_limit * 100, 1), "consecutive": consecutive, }) else: consecutive = 0 ``` The main loop does not isolate failures for individual agents: ```python agent_dirs = sorted(glob.glob(os.path.join(AGENTS_DIR, "*"))) for agent_dir in agent_dirs: if not os.path.isdir(agent_dir): continue dirname = os.path.basename(agent_dir) if dirname == "governance": continue agent_name = AGENT_MAP.get(dirname, dirname) budget_path = os.path.join(agent_dir, "BUDGET.json") if not os.path.exists(budget_path): continue report = audit_agent(agent_dir, agent_name, today) reports.append(report) ``` The initial JSON load is also not protected: ```python with open(budget_path) as f: budget = json.load(f) ``` ### Technical Analysis No schema validation is performed before arithmetic and state transitions. If a stale budget file has `daily_lim ...[truncated 1859 chars]
Remediation
View remediation
Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The skill claims enforcement actions including mesh privilege revocation and broader governance controls, but the described implementation appears to be only an audit/reset script with no demonstrated enforcement mechanism. This mismatch can cause operators to rely on security or budget controls that do not actually exist, leading to uncontrolled spend or unauthorized agent behavior under a false sense of protection.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill explicitly describes reading and writing local budget files, but it does not declare any tool scope or permissions boundary. That omission makes the effective capabilities less transparent to users and orchestration systems, increasing the chance that file access is granted too broadly or reviewed inadequately.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill documentation mentions file writes and privilege revocation without a clear warning that these actions can disrupt agent operation or modify local configuration. In a multi-agent environment, such undocumented side effects can lead to accidental denial of service, broken orchestration, or unsafe deployment by administrators who did not understand the impact.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
93% confidence
Finding

The code sets AEST = ZoneInfo("Australia/Sydney") and the module docstring states resets occur at midnight AEST, which forces a specific locale behavior for all users. Under the policy, locale constraints should either be user-selectable or clearly justified as region-specific.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.