Back to skill

Security audit

Construction PM

Security checks for vulnerabilities and agentic risk

Overview

This construction project-management skill is coherent in purpose, but its shell scripts can turn job fields, email text, or workspace paths into executable Python code.

Review this skill before installing. Its local job database behavior is disclosed and useful, but do not run the current scripts on untrusted emails, externally supplied job fields, or unusual DATA_DIR/OPENCLAW_WORKSPACE paths until the Python invocation is rewritten to pass values as data rather than source code.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/add-job.sh:36
Finding
Python Code Injection Through Job Fields and Database Path<![CDATA[ ## Vulnerability Details **File Location**: `scripts/add-job.sh`, lines 36-103 **Vulnerability Type**: Python source-code injection through shell interpolation **Risk Level**: High ### Vulnerable Code ```bash python3 << PYEOF import json, datetime, sys db_path = "$DB" with open(db_path) as f: data = json.load(f) jobs = data.get("jobs", []) today = datetime.date.today().isoformat() # Find existing job existing = None for i, j in enumerate(jobs): if j.get("number") == "$NUMBER": existing = i break if existing is not None: job = jobs[existing] old_status = job.get("status", "") # Update only provided fields if "$CUSTOMER": job["customer"] = "$CUSTOMER" if "$ADDRESS": job["address"] = "$ADDRESS" if "$PM": job["pm"] = "$PM" if "$VALUE": job["value"] = float("$VALUE") if "$VALUE" else job.get("value", 0) if "$STATUS": job["status"] = "$STATUS" if "$PERMIT_STATUS": job["permit_status"] = "$PERMIT_STATUS" if "$PERMIT_NUMBER": job["permit_number"] = "$PERMIT_NUMBER" if "$NOTES": job["notes"] = "$NOTES" if "$TRADE": job["trade"] = "$TRADE" job["updated"] = today # Log status change new_status = job.get("status", "") if "$STATUS" and new_status != old_status: job.setdefault("history", []).append({ "date": today, "from": old_status, "to": new_status, "note": "$NOTES" or f"Status changed to {new_status}" }) else: job = { "number": "$NUMBER", "customer": "$CUSTOMER" or "Unknown", "address": "$ADDRESS" or "", "pm": "$PM" or "", "value": float("$VALUE") if "$VALUE" else 0, "status": "$STATUS" or "lead", "permit_status": "$PERMIT_STATUS" or "", "permit_number": "$PERMIT_NUMBER" or "", "trade": "$TRADE" or "", "notes": "$NOTES" or "", "created": today, "updated": today, "history": [{"date": today, " ...[truncated 1930 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace the expandable heredoc with a quoted heredoc such as `<<'PYEOF'`. - Pass job fields as command-line arguments, environment variables, stdin JSON, or another data channel rather than embedding them in Python source. - Prefer a structured JSON request read from stdin, then validate each field in Python. - Parse `value` as a finite, non-negative numeric value and reject malformed input. - Validate job status and permit status against explicit allowlists. - Pass the database path through `sys.argv` and resolve it with `pathlib.Path`. - Add regression tests containing double quotes, single quotes, backslashes, newlines, Unicode, and Python-like fragments in every accepted field. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/parse-email.sh:19
Finding
Arbitrary Python Execution Through Untrusted Email Content<![CDATA[ ## Vulnerability Details **File Location**: `scripts/parse-email.sh`, lines 19-21 **Vulnerability Type**: Python source-code injection through email parsing **Risk Level**: High ### Vulnerable Code ```bash python3 << PYEOF import re, json, sys email = """$EMAIL_TEXT""" ``` The heredoc continues through line 89 and executes the generated source with `python3`. ### Technical Analysis The script is explicitly designed to process potentially untrusted email content. It first reads the entire message into `EMAIL_TEXT`, then places that content inside a triple-quoted Python literal in an unquoted heredoc. An email containing a triple-double-quote sequence can close the intended string. Newlines and additional Python statements following that sequence become executable source code. Because the shell performs heredoc expansion before Python parsing, no JSON or Python-string escaping protects the email. This is the highest-exposure injection path in the project because ordinary external email content can reach the vulnerable interpolation point. ### Attack Path 1. An attacker sends or supplies an email intended for processing by `parse-email.sh`. 2. The message includes a triple-double-quote terminator followed by syntactically valid Python statements. 3. A user or Agent invokes `parse-email.sh --file` on that message or pipes the message to the script. 4. The shell substitutes the raw message into the unquoted heredoc. 5. Python interprets the attacker's appended statements as program code and executes them. A safe regression demonstration should inject only a statement that creates a disposable marker in a test directory and must run inside an isolated container. ### Impact Assessment The attacker can execute arbitrary Python and operating-system commands as the account running the Agent. Potential consequences include reading workspace files and credentials, modifying tracked jobs, tampering with other Skill files, deleting data, or starting out ...[truncated 118 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Use a single-quoted heredoc delimiter: `python3 <<'PYEOF'`. - Send the email to Python through stdin without embedding it in source. For file mode, pass the filename through `sys.argv` and read it from Python. - Do not store large untrusted messages in shell variables when direct streaming is possible. - Apply input-size limits to prevent memory exhaustion. - Treat extracted fields as untrusted data and escape them appropriately for every later output context. - Add tests for triple quotes, shell metacharacters, Python syntax, multiline content, binary data, and oversized messages. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pipeline.sh:25
Finding
Python Code Injection Through Pipeline Filters and Workspace Path<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pipeline.sh`, lines 25-36 **Vulnerability Type**: Python source-code injection through shell interpolation **Risk Level**: High ### Vulnerable Code ```bash python3 << PYEOF import json, datetime, sys with open("$DB") as f: data = json.load(f) jobs = data.get("jobs", []) today = datetime.date.today() filter_status = "$FILTER_STATUS" filter_pm = "$FILTER_PM" stale_days = int("$STALE_DAYS") if "$STALE_DAYS" else 0 summary = $( [ "$SUMMARY" = "true" ] && echo "True" || echo "False" ) ``` ### Technical Analysis The script places the values of `--status`, `--pm`, and `--stale` directly into generated Python source. The database path derived from `DATA_DIR` or `OPENCLAW_WORKSPACE` is also interpolated into a Python string. Because the heredoc is unquoted, malicious quotes and newlines in any string-valued argument can escape the intended literal and introduce executable Python statements. The stale threshold also lacks strict shell-side validation, although malformed non-injection values generally produce a Python exception rather than command execution. ### Attack Path 1. An attacker influences a pipeline filter supplied by an Agent, such as a PM name or status. 2. The value closes the generated Python string literal and appends another statement. 3. The shell expands the value while constructing the heredoc. 4. Python parses the injected text as code. 5. The injected operation runs with the Agent's privileges. The same path is possible where an untrusted configuration source controls `DATA_DIR` or `OPENCLAW_WORKSPACE`. ### Impact Assessment Exploitation permits arbitrary code execution within the invoking account's security context. Accessible project data, job records, local configuration, and credentials may be read or modified. The flaw does not independently cross an operating-system privilege boundary. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Quote the heredoc delimiter to disable shell interpolation. - Pass the database path and filter values using `sys.argv` or a JSON object on stdin. - Validate `--stale` with a strict non-negative integer expression before invocation. - Validate status values against the documented status allowlist. - Consider exact matching for PM identifiers unless substring matching is explicitly required. - Add tests using quotes, newlines, backslashes, and Python fragments in status and PM filters. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/briefing.sh:15
Finding
Python Code Injection Through Briefing Threshold or Database Path<![CDATA[ ## Vulnerability Details **File Location**: `scripts/briefing.sh`, lines 15-23 **Vulnerability Type**: Python source-code injection through shell interpolation **Risk Level**: High ### Vulnerable Code ```bash python3 << PYEOF import json, datetime, sys with open("$DB") as f: data = json.load(f) jobs = data.get("jobs", []) today = datetime.date.today() stale_threshold = int("$STALE_DAYS") ``` ### Technical Analysis Both the database path and positional stale-days argument are expanded inside an unquoted heredoc. An attacker-controlled value can terminate its Python string literal and insert new statements. Numeric conversion does not provide protection because Python parses and executes the generated source before `int()` can validate the intended value. There is also a functional mismatch between the documentation, which shows `--stale-days 7`, and the implementation, which directly treats the first positional argument as the threshold. That mismatch is not itself a security vulnerability, but explicit argument parsing would make validation clearer. ### Attack Path 1. An attacker influences the stale threshold passed by an Agent or a workspace path supplied through configuration. 2. The crafted value escapes the generated Python literal. 3. The unquoted heredoc constructs a Python program containing attacker-controlled statements. 4. `python3` executes those statements under the invoking account. ### Impact Assessment Successful exploitation allows arbitrary code execution with access to the same files, environment, and subprocess capabilities as the Agent. This may expose construction records, workspace configuration, credentials available to the process, and other local project data. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Change the heredoc to `<<'PYEOF'`. - Implement explicit option parsing for `--stale-days`. - Reject thresholds that are not bounded non-negative integers. - Pass both the database path and threshold through `sys.argv` rather than source interpolation. - Resolve and validate the database path before opening it. - Add injection regression tests for command-line and environment-derived values. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/permit-check.sh:15
Finding
Python Code Injection Through Permit Threshold or Database Path<![CDATA[ ## Vulnerability Details **File Location**: `scripts/permit-check.sh`, lines 15-23 **Vulnerability Type**: Python source-code injection through shell interpolation **Risk Level**: High ### Vulnerable Code ```bash python3 << PYEOF import json, datetime with open("$DB") as f: data = json.load(f) jobs = data.get("jobs", []) today = datetime.date.today() threshold = int("$THRESHOLD") ``` ### Technical Analysis The permit threshold and database path are embedded directly in generated Python source through an unquoted heredoc. Quotes, newlines, or other Python syntax in these values can escape the intended literals. Calling `int()` is not a sufficient defense because source-code parsing occurs before the conversion. The documentation shows a `--threshold` option, while the implementation treats the first argument directly as the threshold. Explicit option parsing and validation are needed. ### Attack Path 1. An attacker influences a threshold value or workspace path used by an automated Agent invocation. 2. The value terminates the intended Python string and injects an additional statement. 3. Shell heredoc expansion incorporates that statement into the program. 4. Python executes the statement with the invoking process's authority. ### Impact Assessment The flaw can provide arbitrary code execution as the Agent user, including access to local construction records and other readable workspace data. It can also modify files, corrupt reports, or launch subprocesses. No independent privilege-escalation mechanism was found. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Use a quoted heredoc and pass all dynamic values as data. - Parse `--threshold` explicitly and reject missing, negative, excessive, or non-numeric values. - Supply the threshold and database path through `sys.argv`. - Validate that the resolved database path points to the expected data area where appropriate. - Add regression tests for malicious quoting and multiline option values. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/init.sh:19
Finding
Python Code Injection Through Initialization Data Path<![CDATA[ ## Vulnerability Details **File Location**: `scripts/init.sh`, lines 19-27 and 31-37 **Vulnerability Type**: Python source-code injection through environment-derived path **Risk Level**: High ### Vulnerable Code ```bash if command -v python3 &>/dev/null; then python3 -c " import json, datetime with open('$DATA_DIR/jobs.json', 'r+') as f: d = json.load(f) d['metadata']['created'] = datetime.date.today().isoformat() f.seek(0); json.dump(d, f, indent=2); f.truncate() " fi ``` The existing-database branch repeats the same unsafe pattern: ```bash python3 -c " import json with open('$DATA_DIR/jobs.json') as f: d = json.load(f) jobs = d.get('jobs', []) print(f' {len(jobs)} jobs tracked') " ``` ### Technical Analysis `DATA_DIR` is derived from the `DATA_DIR` or `OPENCLAW_WORKSPACE` environment variable and is inserted directly into single-quoted Python literals inside a shell-expanded `python3 -c` string. A path containing a single quote, newline, and Python syntax can close the path literal and introduce executable statements. Shell quoting around directory operations does not sanitize the value for use as Python source. This path requires influence over environment-derived configuration rather than ordinary job data, but it remains exploitable in wrappers, automation, or Agent environments that construct workspace paths from externally influenced values. ### Attack Path 1. An attacker causes an automated invocation to use a crafted `DATA_DIR` or `OPENCLAW_WORKSPACE` value. 2. `init.sh` creates or locates the database using that value. 3. The value is embedded in the `python3 -c` source without Python escaping. 4. The crafted path closes the single-quoted literal and adds a Python statement. 5. Python executes the injected statement with the invoking Agent's privileges. ### Impact Assessment Exploitation permits arbitrary code execution in the security context of the process running initialization. The attacker may acc ...[truncated 141 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Never concatenate a path into a `python3 -c` program. - Pass the database path as an argument, for example by invoking a fixed Python program with `python3 - "$DATA_DIR/jobs.json"`. - Use a quoted heredoc for the fixed program and read the path from `sys.argv[1]`. - Resolve the path with `pathlib.Path` and enforce an expected workspace boundary if the execution environment requires it. - Reject paths containing null bytes and handle unusual valid characters as data rather than attempting ad hoc escaping. - Add tests for paths containing spaces, quotes, newlines, backslashes, and Unicode characters. ]]>
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • 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 (3)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill advertises shell-based workflows that initialize a database and write job/briefing data to the filesystem, but the manifest does not declare any tool scope such as permissions or allowed-tools. That mismatch weakens policy enforcement and increases the chance an agent can perform file writes without an explicit, reviewable declaration of that capability.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
This code performs a direct write to the persistent jobs.json database, which can modify or overwrite user data. Although the script prints a success message afterward, there is no prior warning, confirmation, or inline disclosure that running the command will rewrite the database file.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The skill description defines multiple broad, loosely bounded use cases such as tracking jobs, generating briefings, parsing emails, and monitoring pipeline status, which increases the chance an agent will invoke the skill in contexts where it has not been narrowly justified. In practice, overly broad activation criteria can expose project, financial, and email-derived data to unintended processing paths and make risky script execution more likely without clear user intent.