Back to skill

Security audit

Article Fetcher(文章抓取+Notion/Obsidian知识库存档)

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it says, but it should be reviewed because untrusted image URLs can be fetched from the runtime and uploaded to OSS, and LLM traffic is not forced to HTTPS.

Install only if you control the inputs and the configured cloud accounts. Avoid processing untrusted HTML/MHTML until image URL validation is added, use a least-privilege OSS bucket, configure LLM_BASE_URL with HTTPS only, and prefer the pinned requirements.txt installation path.

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

T09 · Insecure Skill Coding Practices

Error
Location
processors/image_processor.py:68
Finding
Unvalidated Image URLs Enable Server-Side Request Forgery and Response Exfiltration to OSS<![CDATA[ ## Vulnerability Details **File Location**: `fetchers/offline_parser.py:105-121`, `main.py:37-45`, `processors/image_processor.py:42-44,68-80` **Vulnerability Type**: Server-Side Request Forgery with response exfiltration **Risk Level**: High ### Vulnerable Code ```python # fetchers/offline_parser.py:105-121 def _fix_and_collect_images(container: BeautifulSoup) -> List[str]: images: List[str] = [] seen = set() for img in container.find_all('img'): ds = (img.get('data-src') or '').strip() src = (img.get('src') or '').strip() real = ds or src if not real or real.startswith('data:image'): continue real = real.split('?')[0] img['src'] = real if 'data-src' in img.attrs: del img['data-src'] if real in seen: continue seen.add(real) images.append(real) logger.debug(f"Extracted {len(images)} images") return images ``` ```python # main.py:37-45 image_urls = article_data.get('images', []) if image_urls: logger.info(f"Found {len(image_urls)} images; starting upload") url_mapping = image_processor.upload_images( image_urls, platform, article_id, article_url=url ) ``` ```python # processors/image_processor.py:42-44 response = self._download_image(img_url, platform_referer) result = self.bucket.put_object(oss_path, response.content) ``` ```python # processors/image_processor.py:68-80 @staticmethod def _download_image(img_url: str, fallback_referer: str): try: resp = requests.get(img_url, timeout=30) if resp.status_code != 403: resp.raise_for_status() return resp except requests.HTTPError: pass if fallback_referer: resp = requests.get( img_url, headers={'Referer': fallback_referer}, timeout=30 ) resp.raise_for_status() return resp raise RuntimeError(f"I ...[truncated 2539 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse every image URL and allow only `https` URLs. Reject `http`, `file`, `ftp`, `gopher`, protocol-relative URLs, malformed URLs, and URLs containing user information. 2. Resolve the destination hostname before connecting and reject every address in loopback, private, link-local, multicast, reserved, and unspecified ranges for both IPv4 and IPv6. 3. Explicitly block common metadata destinations, including `169.254.169.254` and platform-specific metadata hostnames. 4. Disable automatic redirects. If redirects are needed, process them manually and repeat scheme, hostname, DNS, and IP validation for every hop. 5. Protect against DNS rebinding by ensuring the validated address is the address used for the connection. 6. Require an image MIME type from an explicit allowlist, such as `image/jpeg`, `image/png`, `image/gif`, and `image/webp`. 7. Stream downloads with a strict maximum byte count instead of loading an unlimited response into memory. 8. Consider allowing only known image CDN domains associated with each supported platform. 9. Do not upload a response to OSS unless all URL, status, MIME-type, and size checks pass. 10. Add regression tests covering loopback, RFC1918 addresses, IPv6 local addresses, metadata endpoints, encoded IP representations, DNS rebinding, and redirects to internal destinations. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
utils/tag_extractor.py:77
Finding
LLM API Key and Article Content May Be Transmitted over Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `config.py:38-40,59-61`, `utils/tag_extractor.py:77-96` **Vulnerability Type**: Plaintext transmission of credentials and article data **Risk Level**: Medium ### Vulnerable Code ```python # config.py:38-40 self.llm_api_key = os.getenv('LLM_API_KEY', '').strip() self.llm_base_url = os.getenv('LLM_BASE_URL', '').strip() self.llm_model = os.getenv('LLM_MODEL', '').strip() ``` ```python # config.py:59-61 @property def llm_available(self) -> bool: """Whether LLM tag extraction is available""" return bool(self.llm_api_key and self.llm_base_url and self.llm_model) ``` ```python # utils/tag_extractor.py:77-96 endpoint = f"{config.llm_base_url.rstrip('/')}/chat/completions" headers = { 'Authorization': f'Bearer {config.llm_api_key}', 'Content-Type': 'application/json', } body = { 'model': config.llm_model, 'messages': [ {'role': 'system', 'content': system_prompt}, {'role': 'user', 'content': user_prompt}, ], 'stream': False, } for attempt in range(3): try: timeout = [60, 90, 120][attempt] response = requests.post( endpoint, headers=headers, json=body, timeout=timeout ) ``` ### Technical Analysis The LLM integration treats any non-empty `LLM_BASE_URL` as valid. No validation requires HTTPS before the bearer token and article content are transmitted. If the base URL uses `http://`, the following information traverses the network without transport encryption: - `LLM_API_KEY` in the `Authorization` header - Configured model name - Article title - Extracted article text included in the prompt The implementation also relies on the default redirect behavior of `requests` without explicitly restricting redirect schemes or destinations. Although the LLM transmission is documented as optional, configuring the integration should not permit plaintext transmission of secrets. ### Attack Path 1. ...[truncated 1116 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `LLM_BASE_URL` with `urllib.parse.urlparse`. 2. Require the scheme to be exactly `https`. 3. Reject URLs containing embedded user information, fragments, malformed hostnames, or unsupported ports. 4. Consider an explicit allowlist of approved LLM providers or enterprise gateway hostnames. 5. Disable redirects for authenticated LLM requests or manually validate every redirect before resending. 6. Never send the `Authorization` header to a redirect destination with a different origin. 7. Provide a separate explicit opt-in setting for article-content transmission rather than inferring consent solely from the presence of credentials. 8. Document exactly which portions of the article are transmitted. The current extraction entry point truncates plain text to 8,000 characters before the later 12,000-character limit is applied. 9. Apply API-key scope, quota, and rotation controls at the provider. 10. Fail closed with local tag extraction when endpoint validation fails. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:6
Finding
Skill Metadata Installer Does Not Enforce Audited Dependency Versions<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:6` **Vulnerability Type**: Unpinned dependency installation path **Risk Level**: Medium ### Vulnerable Code ```json "install": [ { "id": "pip", "kind": "pip", "packages": "requests oss2 python-dotenv beautifulsoup4 lxml notion-client markdownify pyyaml", "label": "Install Python dependencies" }, { "id": "playwright", "kind": "shell", "command": "playwright install chromium", "label": "Install Playwright Chromium browser" } ] ``` The repository separately contains pinned versions: ```text # requirements.txt:1-10 requests==2.33.0 beautifulsoup4==4.14.3 oss2==2.19.1 notion-client==3.0.0 python-dotenv==1.2.2 lxml==6.1.0 markdownify==1.1.0 playwright==1.54.0 pyyaml==6.0.2 urllib3==2.7.0 ``` ### Technical Analysis The documented command `pip install -r requirements.txt` uses exact versions. However, the Skill metadata exposes another installation path that names packages without versions. An Agent platform that follows the metadata installer instead of `requirements.txt` will resolve whatever package versions are current at installation time. Those versions may differ from the dependencies audited with this repository. This undermines reproducibility and weakens the Skill's claim that dependencies are locked. It also increases exposure to compromised future releases, unexpected breaking changes, and dependency resolution changes. The observed package names do not appear to be obvious typosquatting names. The issue is the unpinned alternate installation path, not evidence that a currently listed package is malicious. ### Attack Path 1. An Agent platform installs the Skill using the metadata `install` entry. 2. The package manager resolves the latest available versions rather than those in `requirements.txt`. 3. A future compromised, malicious, or unexpectedly incompatible dependency version is selected. 4. Package installation hooks or imported runtime c ...[truncated 678 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin exact versions in the metadata installer so they match `requirements.txt`. 2. Prefer a single installation source, such as: ```bash python3 -m pip install -r requirements.txt ``` 3. Include Playwright in the same locked dependency workflow before running `playwright install chromium`. 4. Generate and verify package hashes using a lockfile or `requirements.txt` entries with `--hash`. 5. Install with `--require-hashes` in automated environments. 6. Use a trusted, controlled Python package index or internal mirror. 7. Add automated checks that fail when metadata dependencies and the lockfile diverge. 8. Periodically audit pinned versions for known vulnerabilities and update them through a reviewed change. 9. Run installation and execution in a sandbox with only the environment variables, files, and network destinations required by the Skill. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • 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 (35)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
该代码块的主要功能是把已经准备好的 article_data 写入 Notion 数据库,并将 HTML 正文转换为 Notion block 结构。它依赖 notion_api_key 和 notion_article_database_id,明确访问的是 Notion 资源。相比声明描述,这只是整个宣称能力中的一个子功能,而且还是“可选 Notion”部分;描述中的主要能力——多平台抓取、OSS 上传、LLM 提词、Obsidian 本地归档——在此代码中均没有体现。因此描述与该代码块实际行为存在明显不匹配。

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
该代码块是一个“离线输入解析器”,作用是把 HTML 文本或 .mhtml 文件转成 article_data,包含标题、作者、发布日期、正文 HTML 和图片 URL 列表。它确实与文章处理管线相关,但只覆盖前期的离线解析环节,且实现上明显偏微信页面结构,并带有少量通用回退选择器。声明描述的是一个端到端技能:抓取多个平台文章、上传 OSS 图片、用 LLM 提取关键词、归档到 Obsidian/Notion。当前代码块没有网络抓取逻辑、没有 OSS 调用、没有 LLM 调用、没有本地知识库或 Notion 写入逻辑,因此与声明存在实质性能力不匹配。

Tp4

High
Category
MCP Tool Poisoning
Confidence
88% confidence
Finding
声明描述的是一个覆盖多个平台的完整知识归档流水线,而这段代码只包含微信抓取器的单一子功能。其实际能力局限于访问微信公众号文章页面、注入 cookies、必要时使用 Playwright 抓取 HTML,并解析文章元数据与图片 URL。代码中没有任何 OSS 上传、LLM 调用、Obsidian/Notion 写入逻辑,也没有涉及小红书、豆瓣、知乎抓取。因此就该代码块本身而言,描述未准确代表其实际行为,存在明显范围不一致。

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
该代码片段的实际功能仅限于“小红书文章抓取”。它通过抓取页面 HTML、解析 window.__INITIAL_STATE__ 或降级解析 DOM,提取文章元数据和图片 URL。声明描述的是一个更完整的多平台采集与归档系统,还包括 OSS 上传、LLM 提取关键词、Obsidian/Notion 存档等能力;这些能力在当前代码中均未出现。虽然代码行为属于声明中“抓取小红书”的一个子集,但就‘描述是否准确代表该代码块实际做什么’而言,声明明显比实际代码宽泛得多,且多个核心能力缺失,因此应判定为不匹配。

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
代码行为与整体声明用途存在明显不一致。声明描述的是一个端到端知识归档工具,包含多平台抓取、图片上传到 OSS、LLM 关键词提取,以及同步到 Obsidian/Notion。当前代码块实际上只是一个知乎抓取模块,负责获取知乎页面 HTML 并解析出内容与图片链接,没有执行 OSS 上传、LLM 调用、知识库写入,也没有体现其他平台抓取逻辑。因此从“该技能声明是否准确代表此代码块实际行为”角度看,应判定为不匹配。

Credential Access

High
Category
Privilege Escalation
Content
import os
from dotenv import load_dotenv

# .env 加载策略:$AGENT_HOME > $HERMES_HOME > 当前目录
_agent_home = os.getenv('AGENT_HOME', os.getenv('HERMES_HOME', ''))
if _agent_home:
    load_dotenv(os.path.join(_agent_home, '.env'))
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
# .env 加载策略:$AGENT_HOME > $HERMES_HOME > 当前目录
_agent_home = os.getenv('AGENT_HOME', os.getenv('HERMES_HOME', ''))
if _agent_home:
    load_dotenv(os.path.join(_agent_home, '.env'))
else:
    load_dotenv()
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The primary skill description and operational guidance are written in Chinese, and the document does not indicate multilingual support or provide an English alternative. Under the policy criteria, this is a natural-language locale/language constraint presented without user choice or explicit justification.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
The description forces a specific language/locale in the skill's user-facing documentation. Under the policy, language constraints should either be optional or clearly justified; this file does not offer a language choice or note that the skill is intended only for Chinese-speaking users.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code file accesses a sensitive credential via `config.notion_api_key` and uses it to send article metadata and content to Notion. While there is an error log and docstrings describing archiving behavior, there is no explicit user-facing warning, confirmation, or visible disclosure about credential use or external data transmission in this file.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The calls to `self.notion.pages.create` and `self.notion.blocks.children.append` send user-supplied article data to an external SaaS service. The code lacks a print/log/confirmation at the point of transmission, so users may not be clearly warned that their content and referenced image URLs are being uploaded to Notion.

Natural-Language Policy Violations

Medium
Confidence
92% confidence
Finding
The module docstring and class/method descriptions are written entirely in Chinese and present the skill as a Chinese-language archiver, with no indication that users may choose another language or locale. Under the stated policy, forcing a specific language without opt-in is a natural-language policy violation unless the locale restriction is clearly justified.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
This file contains user-facing natural-language content in a single language, such as the module docstring and later validation message, with no indication that Chinese is optional or required for a region-specific skill. That can violate language/locale policy because it forces one locale without user opt-in.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
The raised ValueError includes a Chinese-only message that users will see when required environment variables are missing. Because no language selection or documented locale restriction is present, this is a natural-language policy concern.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The fetcher loads cookies from a local file, applies matching cookies to outbound request headers, and then sends them in an HTTP request. Although there are debug logs for developers, there is no user-facing prompt, warning comment, or disclosure that authentication/session cookies may be transmitted to remote sites.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
This Python file contains natural-language strings such as the module/class docstrings and the error message entirely in Chinese. Under the policy, forcing a specific language without user opt-in or a clearly documented locale justification is a natural-language policy violation.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code loads and injects cookies from `self.cookies_list` into a Playwright browser context, which is a sensitive credential-handling operation. Although there is debug logging about the number of cookies injected, there is no user-facing warning, confirmation, or explanatory comment disclosing that stored authentication cookies will be used for browser-based access.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The code downloads remote images with requests and then uploads their contents to Alibaba OSS, which transmits user-supplied or system-handled data over the network. Although there is internal logging for success and failure, there is no user-facing prompt, warning comment, or docstring disclosure that image URLs and content will be fetched from third parties and stored in OSS.

Cloud Storage Exfiltration

Medium
Category
Data Exfiltration
Content
# 下载:优先空 Referer,403 时降级回退平台 Referer
                response = self._download_image(img_url, platform_referer)

                result = self.bucket.put_object(oss_path, response.content)

                if result.status == 200:
                    endpoint = config.aliyun_oss_endpoint.lstrip('https://').lstrip('http://')
Confidence
55% confidence
Finding
Data is uploaded to cloud storage (S3 / GCS / Azure Blob). This may be a legitimate backup or exfiltration to an external bucket. Manual review is recommended.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
The code hard-codes `Accept-Language: zh-CN,zh;q=0.9,en;q=0.8`, which imposes a specific locale preference on all outgoing requests. This is a natural-language/locale policy concern because users are not given an opt-in or configuration choice, and the file does not document a region-specific justification for forcing Chinese as the default locale.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The module-level natural-language description is written only in Chinese, which imposes a specific language choice without any visible opt-in or alternative. Under the policy, language constraints should either offer user choice or be clearly documented as justified and region-specific.

Natural-Language Policy Violations

Medium
Confidence
92% confidence
Finding
This code embeds user-facing natural-language behavior entirely in Chinese, including the module description and the LLM system/user prompts. Under the policy, forcing a specific language without user opt-in is a natural-language policy violation unless the locale constraint is explicitly justified, which is not present here.

External Transmission

Medium
Category
Data Exfiltration
Content
for attempt in range(3):
        try:
            timeout = [60, 90, 120][attempt]
            response = requests.post(endpoint, headers=headers, json=body, timeout=timeout)

            if response.status_code == 200:
                return response.json()['choices'][0]['message']['content']
Confidence
91% confidence
Finding
The module sends article text and optionally the title to a user-configured external LLM endpoint, which creates a real data exfiltration path. Even though the feature is optional and truncated to 12,000 characters, the code transmits potentially sensitive article content and bearer credentials to whatever base URL is configured, with no allowlist, trust validation, redaction, or explicit per-request consent.

Natural-Language Policy Violations

Low
Confidence
82% confidence
Finding
This markdown file contains user-facing natural-language content exclusively in Chinese across its substantive entries. Under the policy rule for language/locale, a forced language without user opt-in can be a policy violation when no alternative language choice or justification is provided in the file.

Static analysis

No suspicious patterns detected.