T09 · Insecure Skill Coding Practices
- Location
scripts/summarize_weather.py:101- Finding
Shell Command Injection Through Configurable Summary Executable
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill mostly performs a weather dashboard workflow, but its security claims do not match the shipped code and it can run configurable local executables.
Review this skill before installing. Its weather and dashboard functions are understandable, but do not run it in a sensitive environment unless SUMMARIZE_BIN, OBSIDIAN_BIN, CANVAS_ROOT, and Obsidian paths are locked down and the shell=True summarize call is fixed. Be aware that the dashboard loads third-party web resources and that configured coordinates may be sent to Open-Meteo.
scripts/summarize_weather.py:101Shell Command Injection Through Configurable Summary Executable
scripts/flush_to_obsidian.py:57Configurable Arbitrary Executable and Unvalidated Obsidian Destination
scripts/dashboard.html:6Dashboard Executes Remotely Hosted JavaScript Without Integrity Protection
scripts/fetch_weather.py:43Configurable Location Coordinates Are Transmitted Contrary to the Declared Fixed Scope
The skill description claims constrained behavior, but the analysis indicates the implementation omits the advertised summarization and Obsidian write steps while also storing additional local state and raw weather data. This mismatch undermines operator trust and can hide unexpected persistence or data handling that users did not consent to.
This is tool parameter abuse because the subprocess tool is invoked with a shell command string instead of a literal argument vector. In the context of a local AI/automation skill, this is more dangerous because environment or configuration tampering can turn a weather summarization step into arbitrary local code execution on the host.
print(f"[summarize] URL: {url}")
try:
result = subprocess.run(
cmd_str,
capture_output=True,
text=True,
The skill requests users to run Python scripts that fetch network data, invoke a local CLI, and write local files, but it does not declare any explicit tool scope or allowed-tools constraints. In an agent ecosystem, missing permission boundaries increases the chance of overbroad tool access and makes it harder to enforce least privilege if the skill is modified or abused.
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.
- Do **not** modify `HEARTBEAT.md`.
- Do **not** change global OpenClaw config.
- Do **not** create or run `.bat`, `.cmd`, or `.ps1` files.
- Do **not** use Windows Task Scheduler, startup folders, registry persistence, or shell profile persistence.
- Do **not** read generic secret-bearing files such as `env.bat`.
- Only run the Python scripts bundled with this skill.
The dashboard pulls executable JavaScript from cdnjs and fonts from Google at runtime, which introduces unnecessary external network dependencies into a skill described as local. If the CDN, network path, or hosting account is compromised, users could receive altered code or leak usage metadata despite expecting a local-only dashboard.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
wind_unit = "mph" if UNITS == "imperial" else "kmh"
precip_unit = "inch" if UNITS == "imperial" else "mm"
base = "https://api.open-meteo.com/v1/forecast"
params = (
f"latitude={LAT}&longitude={LON}"
f"&timezone={TZ}"
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
wind_unit = "mph" if UNITS == "imperial" else "kmh"
precip_unit = "inch" if UNITS == "imperial" else "mm"
base = "https://api.open-meteo.com/v1/forecast"
params = (
f"latitude={LAT}&longitude={LON}"
f"&timezone={TZ}"
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
with open(TIMESERIES_FILE, "r", encoding=enc) as f:
data = json.load(f)
if enc != "utf-8":
with open(TIMESERIES_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return data
except (json.JSONDecodeError, UnicodeDecodeError, IOError):
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
with open(TIMESERIES_FILE, "r", encoding=enc) as f:
data = json.load(f)
if enc != "utf-8":
with open(TIMESERIES_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return data
except (json.JSONDecodeError, UnicodeDecodeError, IOError):
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
cmd.extend(["--content", formatted, "--append"])
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=30, encoding="utf-8",
)
if result.returncode != 0:
OBSIDIAN_BIN is taken directly from the environment and executed as a program, so anyone who can influence the runtime environment can cause the script to launch an arbitrary executable instead of the intended obsidian-cli. In a local automation skill this is less severe than remote input-driven RCE, but it still creates a code-execution foothold in misconfigured or multi-user environments.
cmd.extend(["--content", formatted, "--append"])
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=30, encoding="utf-8",
)
if result.returncode != 0:
The destination path for shutil.copy2 is influenced by CANVAS_ROOT from the environment, and the code writes to that computed location without validating that it stays within an expected trusted directory. If an attacker can control the environment for this process, they can redirect the dashboard write to an unintended filesystem location, causing arbitrary file overwrite within the runner's privileges.
def prepare_dashboard() -> None:
os.makedirs(CANVAS_DIR, exist_ok=True)
shutil.copy2(DASHBOARD_SRC, DASHBOARD_DST)
print(f"[runner] Dashboard prepared: {DASHBOARD_DST}")
print(f"[runner] Suggested canvas URL: {BASE_URL}/__openclaw__/canvas/{SKILL_ID}/dashboard.html")
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
script_path = os.path.join(SCRIPT_DIR, script_name)
cmd = [sys.executable, script_path]
print(f"[runner] Running: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=SCRIPT_DIR)
print(f"[runner] Exit code for {script_name}: {result.returncode}")
return result.returncode
The docstring rationalizes shell=True as a safety measure for '&' in the URL, but this is backwards: invoking a shell is what causes '&' to become dangerous command syntax. This misleading comment increases the likelihood the insecure pattern will be retained or copied elsewhere, preserving an actual command-injection flaw.
The code invokes subprocess.run with shell=True on a command string built from environment-controlled data (SUMMARIZE_BIN) and a URL string. This allows shell metacharacter interpretation and can lead to arbitrary command execution if an attacker can influence the environment or related configuration, which is especially risky for an automation skill expected to run unattended.
print(f"[summarize] URL: {url}")
try:
result = subprocess.run(
cmd_str,
capture_output=True,
text=True,
cmd_str is tainted by os.environ.get("SUMMARIZE_BIN", "summarize") and then executed through the shell, so a malicious environment value can inject additional shell commands. Because the skill also concatenates a URL into the same shell command, the attack surface includes both binary selection and shell parsing behavior.
print(f"[summarize] URL: {url}")
try:
result = subprocess.run(
cmd_str,
capture_output=True,
text=True,
The skill's purpose is weather retrieval, local summarization, dashboard updating, and note appending. Pulling UI libraries and fonts from unrelated third-party services adds network capability beyond the core weather workflow and is not explicitly declared in the manifest scope.
The dashboard hard-codes toLocaleTimeString('en-US', ...), which forces U.S. English locale formatting regardless of the user's preferences. This is a natural-language/locale policy concern because the file does not offer a locale choice or document why the locale must be fixed.
This code reads a user-local config.json file and injects allowed values into process environment variables, but it does so without any logging, print statement, or other runtime disclosure. Although the module docstring documents the behavior, the actual safety-relevant operation of loading file-based configuration into environment variables is silent and could affect downstream behavior without the user noticing.
The code sets a hard-coded default locale-specific timezone of "Asia/Shanghai" in a natural-language configuration value. This can violate language/locale policy expectations because users are placed into a specific regional setting unless they explicitly override the environment variable.
No suspicious patterns detected.