Back to skill

Security audit

Weather Pulse

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent weather and air-quality tool, but it can send a weather API key and searched location to any host configured in the environment.

Review this before installing if you will use a real QWeather key. Only set QWEATHER_API_HOST to the exact host from your QWeather console, use dedicated low-quota API keys, and prefer environment variables over a shared .env file. WAQI access is fixed to the documented AQI host, but both providers receive the locations you query.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/weather.py:104
Finding

Unvalidated API Host Allows Credential and Location Disclosure

Content
View full analysis

Vulnerability Details

File Location: scripts/weather.py, lines 42, 104-106, and 205-209
Vulnerability Type: Unvalidated network destination and sensitive query-string transmission
Risk Level: High

Vulnerable Code

python
QW_HOST = os.environ.get("QWEATHER_API_HOST", "")
python
def _resolve_city_name(name):
    """通过 QWeather Geo API 将城市名解析为 CityId."""
    url = f'https://{QW_HOST}/geo/v2/city/lookup'
    params = {'key': QW_KEY, 'location': name}
    data = _json_fetch(url + '?' + urllib.parse.urlencode(params))
python
def fetch_qweather(endpoint_key, location, lang='zh', unit='m'):
    """请求和风天气 API."""
    ep = QW_ENDPOINTS.get(endpoint_key, f'v7/{endpoint_key}')
    url = f'https://{QW_HOST}/{ep}'
    params = {'key': QW_KEY, 'location': location, 'lang': lang, 'unit': unit}
    return _json_fetch(url + '?' + urllib.parse.urlencode(params))

Technical Analysis

The script obtains QW_HOST from the QWEATHER_API_HOST environment variable and interpolates it directly into HTTPS request URLs. It verifies only that the value is nonempty; it does not establish that the hostname is an authorized QWeather domain.

Consequently, any party able to influence the process environment or deployment configuration can redirect weather and city-lookup requests to an arbitrary HTTPS server. The requests include both QW_KEY and the user-supplied location in the URL query string.

TLS only protects the connection to the selected destination. It does not prevent disclosure when the selected destination itself is attacker-controlled. Query-string credentials may also be retained in destination server logs, reverse-proxy logs, monitoring systems, or other URL-processing infrastructure.

This network access is generally necessary for the Skill's declared weather functionality, but permitting an unrestricted credential destination exceeds minimum privilege. The WAQI request doe ...[truncated 1715 chars]

Remediation
View remediation

Remediation Suggestions

  1. Validate the configured destination against an explicit trust policy.

    • Parse the value as a hostname rather than interpolating an unrestricted string.
    • Require an approved QWeather domain or documented domain suffix.
    • Reject IP literals, URL schemes, user-information components, ports, paths, query strings, fragments, control characters, and malformed hostnames.
    • Perform suffix validation on DNS label boundaries; a simple substring or endswith("qweatherapi.com") check may accept deceptive domains if implemented incorrectly.
  2. Prefer a fixed API origin where operationally possible.

    • If QWeather supports a stable endpoint, hardcode it.
    • If account-specific hosts are required, provision the approved hostname through trusted installation configuration and validate it before every request.
  3. Avoid credentials in URL query strings where supported.

    • Use the provider-supported authorization header or another non-URL authentication mechanism.
    • Ensure error messages and debug logs never print authentication values.
  4. Fail closed on invalid configuration.

    • Terminate before making a request when the host does not satisfy the allowlist.
    • Do not silently normalize or accept unexpected host syntax.
  5. Constrain redirects.

    • Reject redirects to unapproved hosts so credentials or sensitive request data cannot be forwarded outside the trusted destination set.
  6. Limit credential impact.

    • Use a dedicated key with the minimum required API permissions and quota.
    • Rotate the key if it may have been used with an untrusted host.
    • Monitor usage for unexpected requests and quota consumption.

Example validation should conceptually enforce a trusted, syntactically valid hostname before constructing the URL:

python
import re

HOST_RE = re.compile(
    r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
    r"(?:\.[a-z0-9](?:[a-z0-9
...[truncated 616 chars]
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (10)

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/weather.py (reported line 176)May include surrounding context.

python
last_err = None
    for attempt in range(1, MAX_RETRIES + 1):
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                raw = resp.read()
                ce = resp.headers.get('Content-Encoding', '')
                if 'gzip' in ce:

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 119)May include surrounding context.

$env:WAQI_API_TOKEN = "your_waqi_token"

text

#### Option B: .env File

Create a `.env` file in the same directory as `weather.py`, and load it before running:

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 124)May include surrounding context.

$env:WAQI_API_TOKEN = "your_waqi_token"

text

#### Option B: .env File

Create a `.env` file in the same directory as `weather.py`, and load it before running:

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 132)May include surrounding context.

$env:WAQI_API_TOKEN = "your_waqi_token"

text

#### Option B: .env File

Create a `.env` file in the same directory as `weather.py`, and load it before running:

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 135)May include surrounding context.

$env:WAQI_API_TOKEN = "your_waqi_token"

text

#### Option B: .env File

Create a `.env` file in the same directory as `weather.py`, and load it before running:

Ae4

Medium
Category
analysis-evasion
Confidence
80% confidence
Finding

Suspicious Unicode normalization or mixed-script content

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The trigger patterns are very broad and match ordinary weather-related conversation such as 'weather', 'temperature', 'rain', and 'humidity'. This can cause the skill to activate in many benign chats, increasing unnecessary access to external APIs and making it easier for a user or another prompt to steer execution into networked tool use unexpectedly.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

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

Content

Scanner excerpt · scripts/weather.py (reported line 223)May include surrounding context.

python
"""请求 WAQI 空气质量 API.
    location: 城市名(xinxiang) / @ID(5789) / geo:35.30;113.92 / here
    """
    url = f'https://api.waqi.info/feed/{urllib.parse.quote(location)}/?token={WAQI_TOKEN}'
    return _json_fetch(url)

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

The module docstring and usage instructions are presented exclusively in Chinese, with no language choice or bilingual alternative. That creates a language-policy issue because the skill's natural-language interface assumes one locale rather than offering user choice.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
93% confidence
Finding

The weather-fetching function sets lang='zh' by default, and the CLI also defaults --lang to zh, which means the skill forces a specific language unless the user knows to override it. This is a natural-language locale policy concern because the skill does not first ask for or negotiate the user's preferred language.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.