Back to skill

Security audit

Weather Digest

Security checks for vulnerabilities and agentic risk

Overview

The skill is a weather report generator using NOAA/NWS data, with optional automation and sharing recipes that should be enabled only with user intent.

Install only if you are comfortable sharing configured latitude/longitude locations with NOAA/NWS. Treat Slack, heartbeat, cron, LaunchAgent, Things, and Reminders examples as optional automation: review destinations, use restricted channels, and know how to disable scheduled jobs. Avoid publishing or embedding the generated HTML until dynamic fields are HTML-escaped, and prefer a pinned dependency set for reproducible installs.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
weather_digest.py:135
Finding
Unescaped Dynamic Data in Generated HTML<![CDATA[ ## Vulnerability Details **File Location**: `weather_digest.py:135-151, 211-221`; mirrored in `weather digest/weather_digest.py:135-151, 211-221` **Vulnerability Type**: HTML injection through unescaped configuration and remote API data **Risk Level**: Medium ### Vulnerable Code ```python def format_alerts_html(alerts: list) -> str: if not alerts: return "<div class=\"no-alerts\">No active alerts</div>" blocks = [] for alert in alerts: event = alert.get("event") or "Alert" severity = alert.get("severity") or "Unknown" expiry = _format_expiration(alert.get("expires")) headline = alert.get("headline") or "Details forthcoming" instructions = _trim_text(alert.get("instructions"), limit=220) expiry_html = f"<div class=\"expires\">Expires {expiry}</div>" if expiry else "" instructions_html = ( f"<div class=\"instructions\">{instructions}</div>" if instructions else "" ) blocks.append( f"<div class=\"alert\"><div class=\"alert-title\"><strong>{event}</strong>" f"<span class=\"pill\">{severity}</span></div>{expiry_html}" f"<div class=\"headline\">{headline}</div>{instructions_html}</div>" ) return "".join(blocks) ``` ```python def build_html(reports: list[dict], theme: str = "midnight") -> str: today = dt.datetime.now().strftime("%A, %B %d %Y") cards = [] for report in reports: city_meta = "" if report.get("city") and report.get("state"): city_meta = f"<div class=\"subhead\">Nearest location: {report['city']}, {report['state']}</div>" summary_li = "".join( f"<li>{line.replace('**', '')}</li>" for line in report["summary_lines"] ) alerts_html = format_alerts_html(report["alerts"]) cards.append( f"<section class=\"card\"><h2>{report['display_name']}</h2>{city_meta}" f"<h3>Outlook</h3><ul>{summary_li}</ul ...[truncated 2617 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Escape every dynamic value before inserting it into HTML: ```python from html import escape safe_event = escape(str(event), quote=True) safe_severity = escape(str(severity), quote=True) safe_expiry = escape(str(expiry), quote=True) safe_headline = escape(str(headline), quote=True) safe_instructions = escape(str(instructions), quote=True) ``` 2. Apply equivalent escaping to `display_name`, `city`, `state`, and every forecast summary: ```python display_name = escape(str(report["display_name"]), quote=True) city = escape(str(report.get("city") or ""), quote=True) state = escape(str(report.get("state") or ""), quote=True) summary_li = "".join( f"<li>{escape(str(line.replace('**', '')), quote=True)}</li>" for line in report["summary_lines"] ) ``` 3. Prefer a template engine with automatic escaping enabled instead of constructing HTML through f-strings. 4. Validate the configuration schema and enforce expected types and reasonable length limits for names and coordinates. 5. Add regression tests covering `<`, `>`, `&`, quotes, malformed tags, event-handler attributes, and script-like payloads. 6. Apply the same correction to both copies of `weather_digest.py` or remove the duplicate implementation to prevent future security fixes from diverging. ]]>

T08 · Insecure Dependencies

Note
Location
requirements.txt:1
Finding
Unbounded and Unhashed Third-Party Dependency<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt:1`; mirrored in `weather digest/requirements.txt:1` **Vulnerability Type**: Non-reproducible dependency resolution without integrity verification **Risk Level**: Low ### Vulnerable Code ```text requests>=2.31.0 ``` The setup documentation instructs users to install this dependency with: ```bash pip install -r requirements.txt ``` ### Technical Analysis The requirement specifies only a minimum version. Consequently, installation can resolve to any later release available from the configured Python package index. No lock file, exact version constraint, or package hash binds installation to the dependency version reviewed during this audit. The package name `requests` is legitimate, and the audit found no evidence of typosquatting, dependency confusion, or an unsafe package source. The concern is supply-chain reproducibility: future versions can change behavior independently of the audited Skill, and package integrity is delegated entirely to the package index and installation environment. ### Attack Path 1. A future dependency release is compromised, malicious, or otherwise introduces unsafe behavior. 2. A user follows the documented installation procedure. 3. Because the requirement permits any version at or above `2.31.0`, `pip` resolves the affected release. 4. The dependency and its transitive dependencies are installed into the Skill environment. 5. Third-party code executes when imported or used by `weather_digest.py`. This path requires a compromised or unsafe dependency release or package distribution channel; no such compromise was identified during the static audit. ### Impact Assessment A compromised dependency executes with the same privileges as the user running the Skill. It could theoretically read user-accessible files, alter generated output, intercept configured coordinates and NOAA/NWS responses, or initiate additional network connections. The current evidence s ...[truncated 301 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin dependencies to reviewed versions rather than using an open-ended minimum: ```text requests==<reviewed-version> ``` 2. Generate a lock file that includes all transitive dependencies. 3. Record package hashes and install with integrity enforcement: ```bash python -m pip install --require-hashes -r requirements.txt ``` 4. Use an isolated virtual environment, as already recommended by the project documentation. 5. Introduce a controlled dependency-update process that includes vulnerability scanning, review, and testing before changing pinned versions. 6. Keep both dependency files synchronized or consolidate them into one authoritative requirements file. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (27)

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill documentation instructs users to run a Python script that reads a configuration file, writes output to an arbitrary path, and makes outbound network requests, but the manifest declares no explicit tool scope such as permissions or allowed-tools. This creates an authorization gap: consumers of the skill cannot easily tell what capabilities the skill expects, and a modified or misused implementation could access files or external resources beyond the intended weather use case.

Context-Inappropriate Capability

Medium
Confidence
86% confidence
Finding
The OpenClaw heartbeat instructions tell the agent to run the digest and direct-message the Markdown file to Dan when severe alerts exist. Direct personal messaging is a separate communication capability not mentioned in the manifest description, which only covers digest generation and alert monitoring.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The manifest describes generating a daily weather digest from NOAA/NWS data with customizable locations and alert monitoring. This section instructs posting digest content to Slack via an arbitrary webhook URL, which expands the skill into outbound messaging/integration behavior not stated in the manifest.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The webhook instructions send digest content to Slack without any warning about external disclosure, retention, or workspace visibility. Even if weather data is usually low sensitivity, digests may include user-specific locations, operational context, or alerting patterns that should not be transmitted off-system without informed consent.

External Transmission

Medium
Category
Data Exfiltration
Content
1. Extend the wrapper to post summaries via webhook:
   ```bash
   SUMMARY=$(head -n 30 outputs/digest-$(date +%Y%m%d).md)
   curl -X POST -H 'Content-type: application/json' \
     --data '{"text":"Weather Digest\n```'     --data '{"text":"Weather Digest\n```\n'$(tail -n +2 outputs/digest-$(date +%Y%m%d).md | head -n 20)\n```"}' $SLACK_WEBHOOK_URL
   ```
   Adjust `tail/head` lines to control preview length. Use Block Kit if you prefer cards.
Confidence
95% confidence
Finding
This command performs external transmission of generated digest content via a Slack webhook. External posting is not inherently malicious, but it creates a real data egress path that could leak user locations, severe-alert monitoring targets, or other operational details if misconfigured or enabled without clear notice.

Session Persistence

Medium
Category
Rogue Agent
Content
Adjust `tail/head` lines to control preview length. Use Block Kit if you prefer cards.

## LaunchAgent (macOS GUI)
- Save this plist at `~/Library/LaunchAgents/com.dan.weatherdigest.plist`:
  ```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">
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
Adjust `tail/head` lines to control preview length. Use Block Kit if you prefer cards.

## LaunchAgent (macOS GUI)
- Save this plist at `~/Library/LaunchAgents/com.dan.weatherdigest.plist`:
  ```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">
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
Adjust `tail/head` lines to control preview length. Use Block Kit if you prefer cards.

## LaunchAgent (macOS GUI)
- Save this plist at `~/Library/LaunchAgents/com.dan.weatherdigest.plist`:
  ```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">
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
Adjust `tail/head` lines to control preview length. Use Block Kit if you prefer cards.

## LaunchAgent (macOS GUI)
- Save this plist at `~/Library/LaunchAgents/com.dan.weatherdigest.plist`:
  ```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">
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
Adjust `tail/head` lines to control preview length. Use Block Kit if you prefer cards.

## LaunchAgent (macOS GUI)
- Save this plist at `~/Library/LaunchAgents/com.dan.weatherdigest.plist`:
  ```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">
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
- Save this plist at `~/Library/LaunchAgents/com.dan.weatherdigest.plist`:
  ```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><string>com.dan.weatherdigest</string>
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
</dict>
  </plist>
  ```
  Load with `launchctl load ~/Library/LaunchAgents/com.dan.weatherdigest.plist`.

## Delivering to Things 3 / Apple Reminders
- Use `things` CLI to drop the Markdown into Today list for manual review:
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.

Tainted flow: 'html' from pathlib.Path.read_text (line 306, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
if args.html_path:
        html = build_html(reports, theme=args.theme)
        html_path = Path(args.html_path)
        html_path.write_text(html)
        print(f"HTML digest written to {html_path} (theme: {args.theme})")
    if args.json_path:
        json_payload = build_json_document(reports)
Confidence
88% confidence
Finding
The generated HTML embeds unescaped data from external NOAA/NWS responses and user-controlled location names directly into HTML elements. If the resulting file is opened in a browser, malicious HTML or script content in alert headlines, instructions, city names, or display names could execute as stored XSS in the local report.

External Transmission

Medium
Category
Data Exfiltration
Content
def get_point_metadata(lat: float, lon: float) -> dict:
    url = f"https://api.weather.gov/points/{lat},{lon}"
    data = fetch_json(url)
    return data["properties"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def get_point_metadata(lat: float, lon: float) -> dict:
    url = f"https://api.weather.gov/points/{lat},{lon}"
    data = fetch_json(url)
    return data["properties"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def get_point_metadata(lat: float, lon: float) -> dict:
    url = f"https://api.weather.gov/points/{lat},{lon}"
    data = fetch_json(url)
    return data["properties"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def get_point_metadata(lat: float, lon: float) -> dict:
    url = f"https://api.weather.gov/points/{lat},{lon}"
    data = fetch_json(url)
    return data["properties"]
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'markdown' from pathlib.Path.read_text (line 301, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
reports = gather_reports(locations)
    markdown = build_markdown(reports)
    output_path = Path(args.output)
    output_path.write_text(markdown)
    print(f"Markdown digest written to {output_path}")
    if args.html_path:
        html = build_html(reports, theme=args.theme)
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'markdown' from pathlib.Path.read_text (line 301, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
reports = gather_reports(locations)
    markdown = build_markdown(reports)
    output_path = Path(args.output)
    output_path.write_text(markdown)
    print(f"Markdown digest written to {output_path}")
    if args.html_path:
        html = build_html(reports, theme=args.theme)
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'html' from pathlib.Path.read_text (line 306, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
if args.html_path:
        html = build_html(reports, theme=args.theme)
        html_path = Path(args.html_path)
        html_path.write_text(html)
        print(f"HTML digest written to {html_path} (theme: {args.theme})")
    if args.json_path:
        json_payload = build_json_document(reports)
Confidence
87% confidence
Finding
The HTML output embeds untrusted external data from the NOAA/NWS API directly into HTML without escaping fields such as display_name, city/state, headline, event, and instructions. If that upstream data or the config content contains HTML/script payloads, opening the generated report in a browser could trigger stored XSS in the local report.

Tainted flow: 'json_payload' from pathlib.Path.read_text (line 311, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
if args.json_path:
        json_payload = build_json_document(reports)
        json_path = Path(args.json_path)
        json_path.write_text(json.dumps(json_payload, indent=2))
        print(f"JSON digest written to {json_path}")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'json_payload' from pathlib.Path.read_text (line 311, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
if args.json_path:
        json_payload = build_json_document(reports)
        json_path = Path(args.json_path)
        json_path.write_text(json.dumps(json_payload, indent=2))
        print(f"JSON digest written to {json_path}")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Unpinned Dependencies

Low
Category
Supply Chain
Content
requests>=2.31.0
Confidence
96% confidence
Finding
The dependency is specified as `requests>=2.31.0`, which allows future releases to be installed without review and prevents reproducible builds. This can introduce vulnerable or breaking versions over time, especially for a network-facing skill that likely fetches external NOAA/NWS data.

Unverifiable Dependency: requests has 16 known advisory(ies) (CVE-2014-1830 (Exposure of Sensitive Information to an Unauthorized Actor in Requests); CVE-2024-47081 (Requests vulnerable to .netrc credentials leak via malicious URLs); CVE-2024-35195 (Requests `Session` object does not verify requests after making first request wi) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
91% confidence
Finding
The manifest references `requests` without pinning an exact version, so there is no way to verify whether the installed package includes fixes for known advisories affecting some releases. Because this skill likely performs outbound HTTP requests, using an unverified version of `requests` increases the risk of exposure to request-handling or credential-leak issues if a vulnerable release is resolved at install time.

Context-Inappropriate Capability

Low
Confidence
84% confidence
Finding
The manifest describes a weather digest generator with alert monitoring, but this section extends the skill into task/reminder creation in external productivity tools. That capability is not an obvious requirement for producing weather reports and represents an unjustified expansion of scope.

Static analysis

No suspicious patterns detected.