Back to skill

Security audit

news-digest

Security checks for vulnerabilities and agentic risk

Overview

This appears to be a real Chinese news digest tool, but its broad web fetching, disabled TLS checks, and optional LLM credential/content transmission need review before installation.

Install only if you are comfortable with the skill scraping external websites, keeping a local SQLite database of full article content, and writing digest files locally. Do not enable the LLM stage with valuable credentials unless the endpoint is HTTPS and trusted, and avoid running it on sensitive internal networks until URL allowlisting, redirect/IP validation, TLS verification, retention controls, and the missing package files are fixed.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/news_digest_v2/fetcher.py:734
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/news_digest_v2/fetcher.py:734-750`, `scripts/news_digest_v2/fetcher.py:785`, and `scripts/news_digest_v2/fetcher.py:995` **Vulnerability Type**: Server-Side Request Forgery through unvalidated source and article URLs **Risk Level**: High ### Vulnerable Code ```python for a in soup.find_all('a', href=True): url = a['href'] title = a.get_text(strip=True) if not title or len(title) < 5: continue # Convert to an absolute URL if url.startswith('//'): url = 'https:' + url elif not url.startswith('http'): url = urljoin(base_url, url) if not url.startswith('http'): continue if url in seen_urls: continue seen_urls.add(url) links.append({'title': title, 'url': url}) ``` The resulting URLs and database-configured source URLs are subsequently fetched: ```python def fetch_article_content(url, timeout=8): html = fetch_page(url, timeout=timeout) ``` ```python for site in WEBSITES: html = fetch_page(site['url']) ``` ### Technical Analysis The fetch pipeline accepts URLs from two trust boundaries: 1. Source URLs loaded from the configurable `monitor_websites` SQLite table. 2. Absolute links extracted from the HTML of monitored pages. Before issuing requests, the code does not validate: - The URL scheme. - The destination hostname. - The resolved IP address. - Whether the destination is loopback, private, link-local, multicast, or otherwise reserved. - Whether an article URL remains on an approved news-source domain. - The destination of HTTP redirects. - The destination port. The check that a URL begins with `http` is not a sufficient security boundary. It still permits requests to addresses such as `http://127.0.0.1`, private network services, or cloud metadata endpoints. The `requests` library also follows redirects by default, so an initially acceptable URL could redirect to a restricted address unless e ...[truncated 1480 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only `https` URLs unless a narrowly documented exception is essential. 2. Maintain an explicit allowlist of approved news-source hostnames. 3. Require extracted article links to remain on the source hostname or a specifically approved related hostname. 4. Parse every URL with `urllib.parse.urlsplit()` and reject embedded credentials, unexpected ports, malformed hosts, and non-HTTP schemes. 5. Resolve the hostname before connecting and reject all loopback, private, link-local, multicast, unspecified, and reserved IPv4 and IPv6 ranges using Python's `ipaddress` module. 6. Protect against DNS rebinding by ensuring the validated address is the address actually used for the connection. 7. Disable automatic redirects or validate the scheme, hostname, port, and resolved address at every redirect hop. 8. Apply strict response-size and content-type limits before reading response bodies. 9. Treat the SQLite source table as security-sensitive configuration and restrict who can modify the database. 10. Keep the existing response truncation as defense in depth, but do not rely on it as an SSRF control. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/news_digest_v2/fetcher.py:304
Finding
TLS Certificate Verification Is Disabled for News Fetching<![CDATA[ ## Vulnerability Details **File Location**: `scripts/news_digest_v2/fetcher.py:304-329` **Vulnerability Type**: Improper certificate validation and insecure TLS fallback **Risk Level**: High ### Vulnerable Code ```python # Special handling for gov.cn is_gov = 'www.gov.cn' in url current_headers = HEADERS.copy() if is_gov: current_headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36' current_headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' current_headers['Accept-Encoding'] = 'gzip, deflate' # gov.cn may have an SSL policy, try without verification verify_ssl = False else: verify_ssl = True response = requests.get( url, headers=current_headers, timeout=actual_timeout, verify=verify_ssl ) response.raise_for_status() return decode_response(response, url) ``` The generic SSL error handler also retries every affected source without certificate validation: ```python except requests.exceptions.SSLError: try: response = requests.get( url, headers=HEADERS, timeout=timeout, verify=False ) response.raise_for_status() return decode_response(response, url) except Exception as e2: _flog(f" Fetch failed (SSL fallback): {url} - {e2}") return None ``` ### Technical Analysis TLS certificate verification authenticates the remote server and prevents an intermediary from presenting an arbitrary certificate. Setting `verify=False` disables this protection while retaining encryption that is not authenticated. The implementation disables verification in two broad situations: - Every URL containing `www.gov.cn`. - Every HTTPS request that initially raises an SSL verification error. The fallback converts a security failure into an insecure successful request. It therefore defeats the purpose of certificate ...[truncated 1504 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove every use of `verify=False`. 2. Do not retry a certificate validation failure using weaker security settings. 3. Use the operating system's current trusted CA store or a maintained CA bundle. 4. If a specific source requires special trust configuration, install the correct issuing CA or use narrowly scoped certificate/public-key pinning after independently verifying the expected certificate. 5. Fail closed when certificate verification fails and continue processing other sources. 6. Stop globally suppressing `InsecureRequestWarning`; log certificate failures with the affected hostname. 7. Prefer HTTPS sources over HTTP sources wherever HTTPS is available. 8. Combine strict TLS verification with hostname and redirect validation so that redirects cannot bypass destination policy. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/news_digest_v2/stage2_5_llm_summary.py:27
Finding
Configurable LLM Endpoint Can Expose API Credentials over Plaintext Transport<![CDATA[ ## Vulnerability Details **File Location**: `scripts/news_digest_v2/stage2_5_llm_summary.py:27-29`, `scripts/news_digest_v2/stage2_5_llm_summary.py:41-68`, and `scripts/news_digest_v2/stage2_5_llm_summary.py:829-863` **Vulnerability Type**: Unvalidated sensitive-data transmission and indirect prompt injection **Risk Level**: Medium ### Vulnerable Code The endpoint and bearer credential are loaded from environment variables without transport validation: ```python API_KEY = os.environ.get('NEWS_DIGEST_LLM_API_KEY', '') BASE_URL = os.environ.get('NEWS_DIGEST_LLM_BASE_URL', '') MODEL = os.environ.get('NEWS_DIGEST_LLM_MODEL', 'qwen3.6-plus') ``` The API key is then attached to the configured endpoint: ```python req_data = json.dumps({ 'model': MODEL, 'messages': [{'role': 'user', 'content': prompt}], 'temperature': temperature, 'max_tokens': max_tokens }).encode('utf-8') req = urllib.request.Request( f'{BASE_URL}/chat/completions', data=req_data, headers={ 'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json' } ) try: with urllib.request.urlopen(req, timeout=300) as resp: result = json.loads(resp.read().decode('utf-8')) return result.get('choices', [{}])[0].get('message', {}).get('content', '') ``` Untrusted scraped content is directly interpolated into the model prompt: ```python content_snippet = ( n.get('content', '') or n.get('summary', '') )[:800] if has_cyrillic: articles_text += ( f"[{i+1}] {n['source']}:" f"(title encoding abnormal; generate an accurate title from the body)\n" f"{content_snippet}\n\n" ) else: articles_text += ( f"[{i+1}] {n['source']}:{raw_title}\n" f"{content_snippet}\n\n" ) prompt = f"""You are a professional news editor. The following news items require title processing. For each item marked as having an abnormal title encoding, generate an accurate title from the body. Fo ...[truncated 3043 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `NEWS_DIGEST_LLM_BASE_URL` before use and require the `https` scheme. 2. Reject embedded URL credentials, fragments, unexpected ports, and malformed hostnames. 3. Consider an explicit allowlist of approved LLM provider hostnames. 4. Never attach the bearer token to a plaintext HTTP request. 5. Prevent cross-origin redirects, or disable redirects and validate each destination before resending credentials. 6. Keep the API key in a narrowly scoped environment variable or secret manager and grant it only the minimum provider permissions and spending limit. 7. Clearly disclose that qualifying article excerpts are sent to the configured provider. 8. Represent scraped content as explicitly delimited data, with a strong instruction that text inside the data boundary must never be treated as model instructions. 9. Prefer structured JSON input and structured model output where supported. 10. Validate returned titles for maximum length, expected language, prohibited URLs, unexpected instructions, control characters, and formatting violations. 11. Do not send content derived from private, loopback, link-local, or internal destinations; remediate the SSRF issue before enabling external LLM processing. 12. Log the destination hostname and transmission event without logging the API key or complete prompt. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (45)

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

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(req, timeout=300) as resp:
            result = json.loads(resp.read().decode('utf-8'))
            return result.get('choices', [{}])[0].get('message', {}).get('content', '')
    except Exception as e:
Confidence
94% confidence
Finding
The code sends article content and titles to an external endpoint derived from NEWS_DIGEST_LLM_BASE_URL, which is taken directly from environment variables without any allowlisting or validation. In this skill, the payload can include scraped news content and source metadata, so a misconfigured or attacker-controlled base URL can cause silent exfiltration of collected data and API credentials to an unintended service.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The declared description presents a full end-to-end news digest capability: scraping Chinese news sources, processing content, and generating daily summaries with citations/links. The supplied code, however, is a narrow internal processing stage labeled '阶段 2', explicitly handling only deduplication and keyword tagging via database functions (`mark_duplicates`, `update_keywords_for_new`). It prints job progress and counts, and even notes that summary output is moved to stage 3. There is no scraping, summarization, source attribution, or link output in this chunk. While deduplication and keyword tagging are consistent with 'processing,' the overall declared purpose materially overstates what this code actually does, so this chunk does not accurately represent the claimed skill behavior on its own.

Ae1

High
Category
analysis-evasion
Content
- `fetcher.py` 新增 `decode_response()` 函数,对已知 GBK 编码来源(人民日报海外版)强制使用 GBK 解码,从根源修复 Cyrillic 乱码
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `stage2_5_llm_summary.py` 新增乱码标题检测,发现乱码时提示 LLM 从正文生成准确标题
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill documents and invokes capabilities that read environment variables, access local files, write output files, and perform network scraping, but it does not declare any explicit tool scope or permissions boundary. This is dangerous because users and hosting platforms cannot easily evaluate or constrain what the skill is allowed to do, increasing the risk of unintended data access, file writes, or outbound requests during execution.

Natural-Language Policy Violations

Medium
Confidence
87% confidence
Finding
The manifest description explicitly scopes the skill to Chinese news sources and includes Chinese trigger phrases, but does not indicate that users can choose another language or locale. Under the stated policy, forcing a specific language or locale without opt-in is a natural-language policy concern unless the constraint is clearly documented and justified as region-specific.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill states that it produces output files, including a timestamped file on the user's desktop, but does not present this as a clear pre-execution warning in the description or quick-start flow. Unexpected writes outside the workspace can overwrite user data, leak sensitive summaries into synced folders, or violate user expectations about where agent-generated artifacts are stored.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The skill performs external website scraping and optionally sends article content to an LLM service when API settings are configured, but this data flow is not clearly surfaced as a user warning. This is dangerous because users may not realize the skill initiates outbound connections to third-party sites and may transmit scraped content to an external model provider, creating privacy, compliance, and network-risk concerns.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The module docstring explicitly states the skill performs web scraping and saves collected news to a database, which goes beyond the declared behavior of generating a news digest. This kind of capability mismatch is a real security concern because hidden or under-disclosed persistence can enable unauthorized data retention, broaden the attack surface, and violate least-privilege expectations for a summarization skill.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The file’s module docstring and operational messages are entirely in Chinese, which effectively fixes the skill’s language for maintainers and operators. Under the policy, forcing a specific language without opt-in or a documented regional justification is a natural-language policy violation.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This code file contains natural-language documentation exclusively in Chinese, beginning with the module docstring, and the pattern continues throughout comments and messages. Under the stated policy, forcing a specific language without user opt-in or a documented justification is a natural-language policy violation.

Description-Behavior Mismatch

Medium
Confidence
90% confidence
Finding
The manifest describes scraping, processing, and generating daily news digests, but this file creates and maintains a local SQLite database schema and persists article/keyword state. Persistent storage may be an implementation choice, but the manifest does not mention database creation or maintaining long-lived local state, so the code does more than the user-facing description claims.

Description-Behavior Mismatch

Medium
Confidence
85% confidence
Finding
Beyond scraping and summarization, the module actively rewrites article records by marking duplicates and storing similarity scores. This is a substantive record-management behavior that is not surfaced in the manifest, which only promises digest generation from Chinese news sources.

Natural-Language Policy Violations

Medium
Confidence
82% confidence
Finding
The module docstring and embedded natural-language comments/instructions are entirely in Chinese, indicating an implicit fixed-language experience. There is no visible opt-in, language selection, or justification that this skill is intended only for a Chinese-language or region-specific context.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
This code explicitly disables TLS certificate verification for gov.cn requests, which permits man-in-the-middle interception or content tampering by a network attacker. For a scraper that ingests and republishes news content, this can poison downstream summaries, store falsified articles, or cause the system to trust attacker-controlled responses.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
current_headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
            current_headers['Accept-Encoding'] = 'gzip, deflate'
            # gov.cn 可能存在 SSL 策略,尝试不验证
            verify_ssl = False 
        else:
            verify_ssl = True
Confidence
99% confidence
Finding
Setting verify_ssl = False establishes an insecure default for requests to a specific domain and normalizes bypassing TLS protections in production code. In a content aggregation pipeline, that means attacker-modified pages can be accepted as authentic, affecting stored articles, summaries, and any downstream decisions based on them.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
except requests.exceptions.SSLError:
        # SSL 错误时尝试不验证证书
        try:
            response = requests.get(url, headers=HEADERS, timeout=timeout, verify=False)
            response.raise_for_status()
            return decode_response(response, url)
        except Exception as e2:
Confidence
99% confidence
Finding
The SSL error fallback retries requests with verify=False, converting certificate failures into silent insecure connections. This removes the main protection against impersonated servers and lets an attacker inject malicious or misleading content into the news ingestion and summarization pipeline.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code fetches article content from external sites and, when LLM summarization is enabled, sends the title and full content to another component for summarization. While there is internal file logging, there is no user-facing warning, prompt, or comment disclosing that externally sourced content may be transmitted onward for LLM processing.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The fetcher stores full scraped article content in the database, not just the digest output described by the skill metadata. This expands data retention and creates an unnecessary persistence surface: if the database is later exposed or reused, the skill has accumulated more third-party content and potentially sensitive metadata than users would reasonably expect.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The module docstring says this is a "格式化输出模块" and the earlier functions perform text cleanup and output generation, but `save_output` also persists data to disk. That is a behavioral expansion from formatting into filesystem output, which is not reflected in this file's stated intent/documentation.

Tainted flow: 'out_file' from os.environ.get (line 219, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
desktop = os.path.join(os.environ.get('USERPROFILE', ''), 'Desktop')
            today_str = datetime.now().strftime('%Y%m%d_%H%M%S')
            out_file = os.path.join(desktop, f"新闻摘要_{today_str}.txt")
            with open(out_file, 'w', encoding='utf-8') as f:
                f.write(output_text)
            saved_to.append(out_file)
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: 'out_file' from os.environ.get (line 219, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
desktop = os.path.join(os.environ.get('USERPROFILE', ''), 'Desktop')
            today_str = datetime.now().strftime('%Y%m%d_%H%M%S')
            out_file = os.path.join(desktop, f"新闻摘要_{today_str}.txt")
            with open(out_file, 'w', encoding='utf-8') as f:
                f.write(output_text)
            saved_to.append(out_file)
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: 'out_file' from os.environ.get (line 219, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
desktop = os.path.join(os.environ.get('USERPROFILE', ''), 'Desktop')
            today_str = datetime.now().strftime('%Y%m%d_%H%M%S')
            out_file = os.path.join(desktop, f"新闻摘要_{today_str}.txt")
            with open(out_file, 'w', encoding='utf-8') as f:
                f.write(output_text)
            saved_to.append(out_file)
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.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The natural-language strings throughout the file are entirely in Chinese, including status messages and error output, and the script provides no user opt-in or alternative locale. Under the policy, forcing a specific language without user choice is a natural-language policy violation unless the locale constraint is explicitly justified.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The module description is written only in Chinese ("过滤规则配置 - 集中管理所有过滤规则"), and the surrounding comments/docstrings throughout the file are likewise Chinese-only. This imposes a fixed language choice in user-facing natural-language content without offering any language or locale option or documenting a justified regional constraint.

Static analysis

Detected: suspicious.insecure_tls_verification

HTTPS certificate verification is disabled.

Warn
Code
suspicious.insecure_tls_verification
Location
scripts/news_digest_v2/fetcher.py:328