Back to skill

Security audit

Save To Obsidian Publish

Security checks for vulnerabilities and agentic risk

Overview

This skill mostly does what it claims, but it forwards user-supplied article URLs to a third-party reader service by default and fetches arbitrary URLs without network-scope controls.

Install only if you are comfortable sending article URLs, including any query parameters, to a third-party reader service and letting the script fetch remote images into your Obsidian vault. Use it with public, non-sensitive URLs, review the configured Obsidian paths first, and consider disabling or removing the Jina Reader path before using private, signed, intranet, or work-document links.

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
save_article_to_obsidian.py:302
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `save_article_to_obsidian.py:113-136` and `save_article_to_obsidian.py:302-349` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```python def download_image(img_url: str, article_hash: str, img_index: int) -> str: """下载图片到本地""" try: url_hash = get_url_hash(img_url) ext = get_file_extension(img_url) filename = f"{article_hash}_{img_index:03d}_{url_hash}{ext}" subfolder = os.path.join(ATTACHMENTS_DIR, article_hash) os.makedirs(subfolder, exist_ok=True) filepath = os.path.join(subfolder, filename) if os.path.exists(filepath) and os.path.getsize(filepath) > 100: return f"../attachments/{article_hash}/{filename}" cmd = [ "curl", "-s", "-L", "--max-time", "30", "-H", "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "-H", "Referer: https://mp.weixin.qq.com/", "-o", filepath, img_url ] result = subprocess.run(cmd, capture_output=True, text=True) ``` ```python def fetch_with_retry(url: str, max_retries: int = 3) -> str: """带重试的抓取,针对不同站点使用不同策略""" last_error = None site_type = detect_site_type(url) wechat_ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.43" for attempt in range(max_retries): try: if attempt > 0: print(f" 🔄 第 {attempt + 1} 次尝试...") time.sleep(1) if site_type == 'wechat': cmd = [ "curl", "-s", "-L", "--max-time", "30", "-H", f"User-Agent: {wechat_ua}", "-H", "Accept: text/html,application/xhtml+xml", "-H", "Accept-Language: zh-CN", "-H", f"Referer: https://mp.weixin.qq.com/", ...[truncated 3478 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Accept only explicitly supported schemes, normally `http` and `https`. 2. Reject URLs containing user information or malformed authority components. 3. Resolve the destination hostname before connecting and reject every resolved address in loopback, private, link-local, multicast, unspecified, reserved, and other non-public ranges. 4. Explicitly block known metadata destinations, including link-local metadata addresses. 5. Disable automatic redirects or validate the scheme, hostname, and resolved IP address at every redirect hop. 6. Apply the same validation to article URLs and all image URLs extracted from remote content. 7. Consider using a strict hostname allowlist when the supported publishing platforms are known. 8. Enforce response-size and content-type limits before storing downloaded data. 9. Run the skill with restricted network access so it cannot reach private networks or metadata services. 10. Add tests covering direct private addresses, DNS names resolving to private addresses, mixed public/private DNS answers, IPv6 loopback and private ranges, and public-to-private redirects. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
save_article_to_obsidian.py:329
Finding
Article URLs Are Disclosed to a Third-Party Proxy Without Explicit Consent<![CDATA[ ## Vulnerability Details **File Location**: `save_article_to_obsidian.py:329-333` **Vulnerability Type**: Sensitive URL Disclosure to a Third Party **Risk Level**: Medium ### Vulnerable Code ```python # 通用:尝试 Jina Reader try: jina_url = f"https://r.jina.ai/{url}" cmd = ["curl", "-s", "-L", "--max-time", "30", "-H", "User-Agent: Mozilla/5.0", jina_url] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 0 and len(result.stdout) > 100: if not is_anti_scraping(result.stdout): return result.stdout except Exception as e: last_error = e ``` ### Technical Analysis The complete input URL is embedded in a request to `https://r.jina.ai/`. URLs can contain sensitive query parameters, signed download credentials, private document identifiers, invitation tokens, or other bearer-style secrets. The README mentions Jina Reader as a retrieval channel, but the implementation does not request explicit consent before forwarding a URL and does not redact sensitive components. It also does not distinguish public article URLs from private or signed URLs. HTTPS protects the request in transit, but it does not prevent the receiving third party or its infrastructure from observing and potentially logging the submitted URL. ### Attack Path 1. A user supplies a private, signed, or secret-bearing article URL to the skill. 2. `process_single_article()` invokes `fetch_with_retry()` with that URL. 3. The function constructs `https://r.jina.ai/{url}` without removing query parameters or other sensitive components. 4. `curl` sends the resulting request to the third-party Jina Reader service. 5. The third party can observe the complete original URL and may be able to use any embedded token while it remains valid. ### Impact Assessment This issue can disclose private resource locations and URL-embedded credentials to an external service. If the URL acts as a bearer credential, possession of the URL may p ...[truncated 273 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make third-party proxy retrieval disabled by default and require explicit user opt-in. 2. Clearly warn users that the complete URL will be transmitted to an external service. 3. Prefer direct retrieval for public URLs before attempting a third-party proxy. 4. Reject URLs containing embedded user information. 5. Detect potentially sensitive query parameters such as `token`, `key`, `signature`, `sig`, `auth`, and `expires`; refuse proxy submission unless the user explicitly confirms it. 6. Where technically possible, remove nonessential query parameters before proxying. 7. Provide a configuration option that permanently disables all third-party URL forwarding. 8. Document the privacy boundary and relevant third-party data handling considerations. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
save_article_to_obsidian.py:486
Finding
Filename Collisions Can Silently Overwrite Existing Articles<![CDATA[ ## Vulnerability Details **File Location**: `save_article_to_obsidian.py:486-490` and `save_article_to_obsidian.py:550-552` **Vulnerability Type**: Unsafe File Overwrite **Risk Level**: Low ### Vulnerable Code ```python def save_to_obsidian(article: dict, article_hash: str, summary: dict, tags: list, user_note: str = "") -> str: """保存为 md 文件""" safe_title = re.sub(r'[\\/:*?"<>|]', '_', article['title'])[:80] safe_title = re.sub(r'\s+', '_', safe_title).strip('_') date_prefix = datetime.now().strftime("%Y%m%d") filename = f"{date_prefix}_{safe_title}.md" filepath = os.path.join(OBSIDIAN_DIR, filename) ``` ```python os.makedirs(OBSIDIAN_DIR, exist_ok=True) with open(filepath, 'w', encoding='utf-8') as f: f.write(md_content) return filepath ``` ### Technical Analysis The output filename contains only the current date and a normalized, truncated title. Although `save_to_obsidian()` receives an `article_hash` argument, it does not include that value in the filename. Different URLs can have identical titles, and distinct titles can normalize or truncate to the same value. If two such articles are processed on the same date, they resolve to the same path. Opening the path with mode `w` truncates any existing file before writing the new content. URL-based duplicate detection does not prevent this collision because the colliding articles can have different URLs. ### Attack Path 1. A user saves article A, producing a file such as `20260916_Shared_Title.md`. 2. Article B has a different URL but the same normalized title, or a title that becomes identical after character replacement or truncation. 3. URL-based duplicate detection treats article B as new. 4. `save_to_obsidian()` computes the same destination path for article B. 5. The file is opened with mode `w`, truncating article A and replacing it with article B. ### Impact Assessment The issue can cause loss of previously archived article content within the configured ...[truncated 416 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Include the existing `article_hash` in every output filename, for example: ```python filename = f"{date_prefix}_{safe_title}_{article_hash}.md" ``` 2. Before writing, check whether the destination already exists and verify that it belongs to the same source URL. 3. Use exclusive creation mode (`x`) when silent replacement is not intended. 4. If a collision occurs, generate a unique suffix or require explicit user confirmation before overwriting. 5. Write to a temporary file and use an atomic rename after successful completion to reduce partial-write risks. 6. Add regression tests for identical titles, normalization collisions, titles longer than 80 characters, and repeated saves on the same date. ]]>
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (10)

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README describes fetching remote articles and localizing images into Obsidian directories, but it does not explicitly warn users that untrusted remote content will be downloaded and written to local files. This can mislead users about the skill's trust boundary and increases the risk of importing malicious or unsafe content into their knowledge base without informed consent.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The skill documentation is entirely in Chinese and the described output format uses Chinese section headings such as '摘要' and '我的笔记', indicating a language-specific experience. There is no indication that users can choose another language or that the Chinese-only behavior is an intentional, justified regional constraint.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
img_url
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        
        if result.returncode == 0 and os.path.exists(filepath) and os.path.getsize(filepath) > 100:
            return f"../attachments/{article_hash}/{filename}"
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
img_url
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        
        if result.returncode == 0 and os.path.exists(filepath) and os.path.getsize(filepath) > 100:
            return f"../attachments/{article_hash}/{filename}"
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
img_url
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        
        if result.returncode == 0 and os.path.exists(filepath) and os.path.getsize(filepath) > 100:
            return f"../attachments/{article_hash}/{filename}"
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The request header explicitly forces Accept-Language: zh-CN for WeChat fetches, and similar locale forcing appears again in the generic curl path. This imposes a specific locale choice in network behavior without offering the user a language preference or documenting a justified region-specific requirement.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The skill transparently uses a third-party fetch/reader service for arbitrary article URLs without warning the user. In this context, users may supply private links, tokenized URLs, internal documentation links, or otherwise sensitive browsing targets; forwarding them externally can leak confidential metadata or content access patterns.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
try:
                jina_url = f"https://r.jina.ai/{url}"
                cmd = ["curl", "-s", "-L", "--max-time", "30", "-H", "User-Agent: Mozilla/5.0", jina_url]
                result = subprocess.run(cmd, capture_output=True, text=True)
                if result.returncode == 0 and len(result.stdout) > 100:
                    if not is_anti_scraping(result.stdout):
                        return result.stdout
Confidence
88% confidence
Finding
The code sends the user-provided target URL to r.jina.ai, a third-party service, by constructing https://r.jina.ai/{url}. This can disclose private, sensitive, or access-controlled URLs and associated retrieval intent to an external party without explicit consent, which is a real privacy and data-handling issue in the skill context.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The generic fetch path explicitly sets Accept-Language: zh-CN, forcing a specific locale regardless of user preference. The file does not provide a user choice or explain why Chinese locale is required for all requests.

Missing User Warnings

Low
Confidence
88% confidence
Finding
The skill clearly instructs users to configure local Obsidian and attachments directories and describes downloading article content and images into those locations, but it does not prominently warn that running the skill will write potentially large amounts of remote content onto the local filesystem. This is a real transparency and safety issue because users may not realize the persistence, storage growth, or privacy implications of saving untrusted web content and media into a synced knowledge base.

Static analysis

No suspicious patterns detected.