Back to skill

Security audit

Portfolio Drift Monitor

Security checks for vulnerabilities and agentic risk

Overview

This skill mostly matches a Kalshi portfolio monitor, but it includes under-disclosed Slack transmission of sensitive portfolio alerts and agent instructions to persistently modify installed code.

Install only if you are comfortable giving it Kalshi API access, storing portfolio snapshots under ~/.openclaw/state, and reviewing or removing the Slack webhook and Agent Bug-Fix Protocol behavior first. Protect the Kalshi key file and avoid sharing debug output that prints credential values or portfolio state.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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)

T01 · Skill Instruction Hijacking

Error

Agent instructions require unauthorized source modification and Git staging

SKILL.md:197

Remediation

T09 · Insecure Skill Coding Practices

Error

Unrestricted webhook destination can disclose sensitive portfolio information

scripts/portfolio_drift.py:48
None: """Send a notification to Slack webhook. Reads OPENCLAW_SLACK_WEBHOOK env var first, then falls back to slack_webhook_url from ~/.openclaw/config.yaml. If no webhook is configured, prints to stdout and returns silently. Args: message: The message text to send """ # Try env var first webhook_url = os.getenv("OPENCLAW_SLACK_WEBHOOK") # Fall back to config.yaml if not webhook_url: config_path = Path.home() / ".openclaw" / "config.yaml" if config_path.exists() and yaml: try: with open(config_path) as f: config = yaml.safe_load(f) or {} webhook_url = config.get("slack_webhook_url") or config.get("slack", {}).get("webhook_url") except Exception: pass # No webhook configured — just print to stdout if not webhook_url: print(f"[Slack] {message}") return # POST to webhook (catch all exceptions to never crash the monitor) try: payload = json.dumps({"text": message}) req = urllib.request.Request( webhook_url, data=payload.encode("utf-8"), headers={"Content-Type": "application/json"} ) with urllib.request.urlopen(req, timeout=5) as response: response.read() except Exception: # Notification failure should never crash the monitor pass ``` The portfolio-derived message is transmitted here: ```python if drifted_positions: alert_lines = ["🚨 Portfolio Drift Alert\n"] for symbol, drift_pct, details in drifted_positions: alert ...[truncated 3195 chars]

Remediation

T09 · Insecure Skill Coding Practices

Warning

Portfolio snapshot is stored without explicitly restrictive permissions

scripts/portfolio_drift.py:299
None: """ Save current portfolio as baseline for next check. Args: portfolio: Current portfolio state from get_current_portfolio() """ try: with open(self.state_file, "w") as f: json.dump(portfolio, f, indent=2) except Exception as e: print(f"ERROR: Failed to save snapshot: {e}") ``` The parent directory is also created without an explicit private mode: ```python self.state_dir = Path.home() / ".openclaw" / "state" self.state_file = self.state_dir / "portfolio_snapshot.json" self.state_dir.mkdir(parents=True, exist_ok=True) ``` ### Technical Analysis The snapshot contains market tickers, position sides, share counts, exposure, average prices, and P&L values. The code uses normal directory and file creation without explicitly setting `0700` on the directory or `0600` on the file. Consequently, effective permissions depend on the process umask and any pre-existing directory permissions. Under a permissive umask or incorrectly configured parent directory, other local users may be able to read the portfolio snapshot. The file is also written directly rather than through a securely created temporary file followed by an atomic replacement. A failed or interrupted write could leave incomplete state, and less controlled file handling increases the risk of permission inconsistencies. ### Attack Path 1. The monitor runs under a permissive umask, or `~/.openclaw/state` already has broad permissions. 2. The script creates or rewrites `portfolio_snapshot.json` without setting a restrictive mode. 3. Another local account or process with filesystem access reads the snapshot. 4. The reader obtains the user’s stored portfolio compo ...[truncated 511 chars]

Remediation

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (27)

Tainted flow: 'req' from os.getenv (line 76, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
data=payload.encode("utf-8"),
            headers={"Content-Type": "application/json"}
        )
        with urllib.request.urlopen(req, timeout=5) as response:
            response.read()
    except Exception:
        # Notification failure should never crash the monitor
Confidence
95% confidence
Finding

The Slack webhook destination is taken from an environment variable or local config and used directly for an outbound HTTP request without validation or allowlisting. In an agent/skill context, that creates an exfiltration channel: portfolio drift alerts and related trading metadata can be sent to any attacker-controlled URL if the environment or config is poisoned, and the broad exception handling makes this hard to detect.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding

Granting a portfolio drift monitor authority to edit Python source files and stage git commits is unjustified and materially expands its attack surface. If the skill is triggered in an agent context, those permissions could be abused to implant persistence, alter trading logic, or tamper with local repositories under the guise of bug fixing.

Missing User Warnings

Medium
Confidence
89% confidence
Finding

This markdown file tells users to obtain a Kalshi API key and set KALSHI_KEY_ID and KALSHI_KEY_PATH, but it does not include any caution about these being sensitive credentials or advice to protect them. Under the markdown-file criteria for missing user warnings, credential-related behavior that can affect privacy or account security should be disclosed.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill documents capabilities to access environment variables, write local state, and call external APIs, yet it declares no explicit tool or permission scope. That mismatch can cause the agent platform to grant broader powers than users expect, increasing the chance of unintended file, credential, or network access.

Missing User Warnings

Medium
Confidence
88% confidence
Finding

The skill notes persistent state storage, but it does not prominently warn that portfolio position data is written to disk in a local snapshot file. Users may not realize potentially sensitive trading information will persist across sessions and be available to other local processes or users depending on filesystem permissions.

Missing User Warnings

Medium
Confidence
87% confidence
Finding

The skill requires API credentials from environment variables and transmits portfolio data to Kalshi, but it lacks a clear privacy and credential-handling warning. Users may enable it without understanding that secrets are read from the environment and sensitive account data is sent over the network and processed locally.

Vague Triggers

Medium
Confidence
95% confidence
Finding

Broad trigger phrases like 'portfolio alert' or 'kalshi positions' can cause accidental invocation during ordinary conversation. When combined with network access, credential use, and local persistence, unintended runs can expose sensitive data or create snapshots without clear user intent.

Intent-Code Divergence

Medium
Confidence
93% confidence
Finding

The documentation first describes a simple one-call monitoring utility, then later expands to code and git modification behavior. This inconsistency undermines informed consent and makes it easier for risky actions to be smuggled into a low-risk skill profile.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding

The skill is presented as a read-only portfolio monitor, but the later bug-fix protocol authorizes editing source files and modifying installed copies. This creates a dangerous documentation/behavior gap where an agent invoked for monitoring could persist unexpected changes to code on disk.

Session Persistence

Medium
Category
Rogue Agent
Content
## Agent Bug-Fix Protocol

**CRITICAL**: When you (the agent) discover and fix a bug in any script during a conversation, you MUST write the fix to the source file on disk using file editing tools. Showing the fix in chat is NOT sufficient — the next scheduled run starts a fresh session and will not have your conversational fix.

Checklist after finding a bug:
1. Edit the actual `.py` file on disk (e.g., `~/skills/portfolio-drift-monitor/scripts/portfolio_drift.py`)
Confidence
98% confidence
Finding

The instruction to write fixes directly to source files on disk creates persistence beyond the current session, which is unnecessary for a monitoring skill. In an adversarial or mistaken invocation, this enables durable tampering with local code and installed skill copies, potentially affecting future scheduled executions.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content

Security: Keep this file private. Never commit to git. Restrict permissions:

bash
chmod 600 ~/.kalshi/key.pem

Optional Variables

Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content

Method 1: Environment Variables (Recommended for automated)

Set in shell profile or in launchd plist:

bash
# ~/.zshrc or ~/.bash_profile
Confidence
75% 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.

Session Persistence

Medium
Category
Rogue Agent
Content
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
Confidence
75% 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.

Missing User Warnings

Medium
Confidence
91% confidence
Finding

The state-file section documents automatic storage of detailed portfolio data on disk, including positions, shares, pricing, and P&L, but does not warn that this is sensitive financial information. If the file is left with permissive filesystem access, included in backups, synced to cloud storage, or read by other local users/processes, it can leak trading strategy and account exposure. The skill context increases sensitivity because the data reflects real portfolio holdings over time.

Missing User Warnings

Medium
Confidence
95% confidence
Finding

The troubleshooting guidance explicitly tells users to print credential material to the terminal with echo $KALSHI_KEY_ID and cat $KALSHI_KEY_PATH | head -1. While meant for debugging, this can expose secrets or key material to shell history, terminal logs, screen recordings, shared sessions, or support screenshots. In the context of a trading skill using live API credentials, that exposure can enable account misuse if mishandled.

Session Persistence

Medium
Category
Rogue Agent
Content
bash
# Create plist as shown in Method 3 above
# Install:
launchctl load ~/Library/LaunchAgents/com.portfolio-drift-monitor.plist

# Check status:
launchctl list | grep portfolio
Confidence
75% 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.

External Transmission

Medium
Category
Data Exfiltration
Content
private_key = f.read()

            config_obj = Configuration(
                host="https://api.elections.kalshi.com/trade-api/v2"
            )
            config_obj.api_key_id = self.key_id
            config_obj.private_key_pem = private_key
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.