Hefeng Weather - 和风天气

Security checks across malware telemetry and agentic risk

Overview

This is a coherent QWeather weather-query skill with disclosed credential setup and expected network access, though users should configure the API host and stored credentials carefully.

Install only if you are comfortable providing QWeather API credentials. Prefer environment variables or the --no-save option, verify HEFENG_API_HOST points to an official or intended QWeather API domain, and protect ~/.config/qweather/.env with owner-only permissions if you save credentials.

SkillSpector

By NVIDIA
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (31)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
url = f"https://{_api_host}/geo/v2/city/lookup"

    try:
        response = httpx.get(url, headers=_auth_header, params={"location": city})

        if response.status_code != 200:
            logger.error(f"查询城市位置失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params={"location": city})

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
url = f"https://{_api_host}/v7/weather/{days}?location={location_id}"

    try:
        response = httpx.get(url=url, headers=_auth_header)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取天气数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url=url, headers=_auth_header)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": loc_value, "lang": lang, "unit": unit}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取实况天气数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": loc_value, "lang": lang, "unit": unit}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取逐小时天气数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"lang": "zh"}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取空气质量数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"hours": hours, "lang": lang}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取空气质量小时预报数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"days": days, "lang": lang}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取空气质量每日预报数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"lang": lang}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取空气质量监测站数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": location_id, "type": index_types, "lang": "zh"}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取生活指数数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
url = f"https://{_api_host}/v7/warning/now?location={location_id}&lang=zh"

    try:
        response = httpx.get(url, headers=_auth_header)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取预警数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": loc_value, "date": date, "lang": lang}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取太阳天文数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": loc_value, "date": date, "lang": lang}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取月亮天文数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": loc_value, "lang": lang}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取分钟级降水数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": formatted_loc, "lang": lang, "unit": unit}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取格点实时天气数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": formatted_loc, "lang": lang, "unit": unit}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取格点每日天气预报失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": formatted_loc, "lang": lang, "unit": unit}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取格点逐小时天气预报失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"number": str(number), "type": city_type, "lang": lang}

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"获取热门城市数据失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params["city"] = city_location_id

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"POI搜索失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params["city"] = city_location_id

    try:
        response = httpx.get(url, headers=_auth_header, params=params)
        if response.status_code == 200:
            return response.json()
        logger.error(f"POI范围搜索失败 - 状态码: {response.status_code}")
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": location_id, "date": target_date, "lang": lang, "unit": unit}

        try:
            response = httpx.get(url, headers=_auth_header, params=params)
            if response.status_code == 200:
                results[target_date] = response.json()
            else:
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Tainted flow: 'url' from os.environ.get (line 435, credential/environment) → httpx.get (network output)

Critical
Category
Data Flow
Content
params = {"location": location_id, "date": target_date, "lang": lang}

        try:
            response = httpx.get(url, headers=_auth_header, params=params)
            if response.status_code == 200:
                results[target_date] = response.json()
            else:
Confidence
93% confidence
Finding
response = httpx.get(url, headers=_auth_header, params=params)

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill clearly uses environment variables, performs network access, and explicitly offers a configuration path that writes credentials to a local file, yet no corresponding permissions are declared. This weakens least-privilege controls and can cause users or hosting platforms to underestimate the skill's ability to persist secrets and make external requests.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding
The skill metadata describes a weather-query capability, but the body also documents credential configuration, local secret persistence under ~/.config/qweather/.env, optional private-key/JWT handling, and additional data operations beyond the concise declared purpose. This mismatch reduces transparency and may mislead reviewers or users about sensitive behaviors such as writing API credentials to disk and reading private key material.

Description-Behavior Mismatch

Medium
Confidence
86% confidence
Finding
A weather-query skill also implements local credential configuration and persistence, which expands its privilege and attack surface beyond the stated purpose. In a plugin/agent setting this is risky because users may expect read-only query behavior, not local secret storage and mutation.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The skill writes API keys and even private key material to a local .env file, which is unnecessary for ordinary weather queries and increases secret exposure. In shared or weakly protected environments, plaintext persistence can lead to credential theft and long-term compromise.

VirusTotal

65/65 vendors flagged this skill as clean.

View on VirusTotal