T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/qweather_query.py:100
- Finding
- QWeather API key can be disclosed to an unrestricted destination host## Vulnerability Details **File Location**: `scripts/qweather_query.py:100-115`, `scripts/qweather_query.py:151-160` **Vulnerability Type**: Unvalidated network destination receiving a sensitive credential **Risk Level**: Medium ### Vulnerable Code ```python def get_api_host(explicit_host: str | None) -> str: host = explicit_host or environ.get("QWEATHER_API_HOST") if not host: fail("Missing API host. Set --api-host or QWEATHER_API_HOST.") return host def request_json(url: str, api_key: str, timeout_s: float) -> Dict[str, Any]: req = urllib.request.Request( url, headers={ "X-QW-Api-Key": api_key, "Accept": "application/json", "Accept-Encoding": "gzip, deflate", "User-Agent": "qweather-city-weather-skill/1.0", }, ) ``` ```python def search_city(query: str, api_key: str, api_host: str, timeout_s: float, number: int) -> List[Dict[str, Any]]: encoded = urllib.parse.quote(query) url = f"https://{api_host}/geo/v2/city/lookup?location={encoded}&number={number}" data = request_json(url, api_key, timeout_s) locations = data.get("location") or data.get("city") or [] if not isinstance(locations, list): return [] return locations def get_weather(location: str, api_key: str, api_host: str, timeout_s: float) -> Dict[str, Any]: encoded = urllib.parse.quote(location) url = f"https://{api_host}/v7/weather/now?location={encoded}" data = request_json(url, api_key, timeout_s) ``` The unrestricted host behavior is also declared in `SKILL.md:20-22` and `references/qweather-http-contract.md:7-10`. ### Technical Analysis The API host is accepted directly from the `--api-host` argument or `QWEATHER_API_HOST` environment variable. It is interpolated into an HTTPS URL without parsing the value or verifying that the resulting hostname belongs to QWeather. Ever ...[truncated 1860 chars]
- Remediation
- ## Remediation Suggestions 1. Parse the configured value as a hostname rather than interpolating arbitrary text into a URL. 2. Reject user information, paths, query strings, fragments, malformed hostnames, IP literals, and ports not explicitly required by policy. 3. Allow only documented QWeather-owned domains or a narrowly defined administrator-controlled allowlist. 4. Resolve and validate the final request origin before attaching `X-QW-Api-Key`. 5. Disable automatic redirects for credential-bearing requests, or follow redirects only when the scheme remains HTTPS and the destination remains on the validated QWeather origin. 6. Prefer an administrator-controlled full base URL over an unrestricted host supplied on every invocation. 7. Add tests proving that attacker-controlled domains, user-info URL forms, malformed hosts, and cross-origin redirects are rejected before any request containing the API key is transmitted.
