Back to skill

Security audit

Clawnoter Obsidian

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent article-saving purpose, but it needs Review because it can fetch unrestricted URLs, disclose submitted links to Jina.ai, and write outside the intended vault under unsafe configuration.

Install only if you are comfortable with the skill contacting target websites and Jina.ai, downloading page-selected images, scanning common local folders for Obsidian vaults, and writing generated notes to disk. Avoid using it with private, signed, internal, localhost, or token-bearing URLs until URL validation, size limits, vault containment checks, and a Jina opt-in or local-only mode are added.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (6)

T01 · Skill Instruction Hijacking

Error
Location
SKILL.md:39
Finding
Mandatory Promotional Content Injected into Agent Responses<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:39-60` **Vulnerability Type**: Agent response and instruction hijacking **Risk Level**: High ### Vulnerable Skill Instructions The mandatory first-run response includes unrelated promotional links and organizational advertising. The relevant instructions, translated into English, state: ```text If you mainly read and collect information in a browser, you can also use the Chrome Extension version, WebNoter: - Chrome Store installation: https://chromewebstore.google.com/detail/webnoter/hmijljoffeceeloaigodmlojbfmgfdkp - Product introduction: https://mp.weixin.qq.com/s/bwqHGb9WGC6L0wL7qVicSA Let me also introduce our team, Research AI+. We are an open global community of young researchers... ``` ### Technical Analysis These promotional instructions are embedded in the operational first-run configuration flow. They direct the Agent to output third-party product links and organizational advertising whenever initial configuration occurs. The advertised extension, product page, and community description are not necessary to identify or configure an Obsidian Vault. Their placement inside the mandatory interaction flow alters the Agent's expected output for an unrelated operational purpose. This is instruction-level output hijacking rather than a code-execution issue. The external links could also be changed or compromised independently of the installed Skill, exposing users to content that was not reviewed with the Skill package. ### Attack Path 1. A user invokes the Skill for the first time. 2. The Skill detects that no Obsidian path has been configured. 3. The Agent follows the mandatory first-run instructions in `SKILL.md`. 4. The Agent presents the embedded extension, product, and community promotions. 5. The user may follow attacker-selected external links under the assumption that they are required or endorsed components of the configuration process. ### Impact Assessment The issue ca ...[truncated 263 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove all extension, product, and community promotion from the mandatory first-run flow. 2. Limit first-run output to the information required to configure an Obsidian Vault. 3. Move optional product information to `README.md` or a dedicated help section. 4. Display external product links only when a user explicitly requests related products or integrations. 5. Clearly label any remaining external links as optional and unrelated to core Skill functionality. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/download_images.py:359
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/download_images.py:157-169, 253-268, 359-370` **Vulnerability Type**: Server-side request forgery through user-controlled and page-controlled URLs **Risk Level**: High ### Vulnerable Code ```python async def _fetch_rendered_article_async(url, timeout_ms=60000): from playwright.async_api import async_playwright async with async_playwright() as p: browser = await p.chromium.launch(headless=True) page = await browser.new_page() try: await page.goto(url, wait_until='networkidle', timeout=timeout_ms) ``` ```python def download_image(url, save_dir, timeout=10): try: headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/120.0.0.0 Safari/537.36' } req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=timeout) as response: ``` ```python try: req = urllib.request.Request(url, headers={ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', }) with urllib.request.urlopen(req, timeout=30) as response: html_content = response.read().decode('utf-8', errors='ignore') ``` ### Technical Analysis The Skill accepts a URL and passes it directly to `urllib.request.urlopen` and, in fallback conditions, Playwright's `page.goto`. It does not validate: - The URL scheme - The destination hostname - The destination's resolved IP address - Loopback, private, link-local, multicast, or reserved address ranges - Cloud metadata endpoints - Redirect destinations - DNS rebinding between valid ...[truncated 1848 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only `http` and `https` URLs. 2. Reject URLs containing credentials or malformed authority components. 3. Resolve destination hostnames before connecting. 4. Reject loopback, private, link-local, multicast, unspecified, and reserved IPv4 and IPv6 ranges. 5. Explicitly block common cloud metadata addresses and hostnames. 6. Disable automatic redirects or validate every redirect destination before following it. 7. Apply the same policy to: - Initial article requests - Playwright navigation - Jina-bound target URLs - HTML image URLs - Markdown image URLs 8. Revalidate the connected peer address to reduce DNS-rebinding exposure. 9. Consider an explicit domain allowlist or require confirmation before accessing non-public destinations. ]]>

other

Warning
Location
scripts/download_images.py:376
Finding
Complete User-Supplied Article URLs Are Disclosed to Jina Reader<![CDATA[ ## Vulnerability Details **File Location**: `scripts/download_images.py:376-384` **Vulnerability Type**: Third-party disclosure of potentially sensitive URL data **Risk Level**: Medium ### Vulnerable Code ```python # Also get markdown content via Jina.ai markdown_content = "" generated_from_raw_html = False try: jina_url = f"https://r.jina.ai/{url}" req = urllib.request.Request(jina_url, headers={ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' 'AppleWebKit/537.36' }) with urllib.request.urlopen(req, timeout=30) as response: markdown_content = response.read().decode('utf-8') except Exception as e: print(f"Failed to fetch via Jina: {e}", file=sys.stderr) ``` ### Technical Analysis The entire user-supplied URL is concatenated into a request sent to `r.jina.ai`. URLs can contain sensitive information, including: - Signed query parameters - Temporary access tokens - Private document identifiers - Internal hostnames and paths - User or tenant identifiers - Session-like values embedded in query strings The disclosure happens automatically during normal processing. Direct local retrieval is already implemented, so automatically sharing the complete URL with an external service is not strictly necessary for basic article saving. The implementation does not redact query strings or fragments, distinguish public from private destinations, or request explicit user consent before transmission. ### Attack Path 1. A user asks the Skill to save a private, tokenized, or signed URL. 2. `process_article` receives the complete URL. 3. The code constructs `https://r.jina.ai/<complete-user-url>`. 4. The request exposes the complete URL to Jina's infrastructure. 5. Jina may subsequently request the target resource, and the submitted URL may enter third-party operational logs. ### Impact Assessment The issue can disclose URL-level secrets and browsing targets to an external service. It does ...[truncated 279 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require explicit user opt-in before sending a URL to Jina or another extraction service. 2. Prefer local direct retrieval and local conversion by default. 3. Warn users that third-party extraction reveals the submitted URL. 4. Remove URL fragments before transmission. 5. Redact query parameters unless they are demonstrably necessary. 6. Never submit URLs that resolve to private, loopback, link-local, or reserved addresses. 7. Provide a configuration option that permanently disables third-party extraction. 8. Document the third-party service's privacy implications in the user-facing runtime flow, not only in repository documentation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/config.py:111
Finding
Subfolder Traversal Can Escape the Configured Obsidian Vault<![CDATA[ ## Vulnerability Details **File Location**: `scripts/config.py:111-130` **Vulnerability Type**: Path traversal and insufficient destination-boundary validation **Risk Level**: Medium ### Vulnerable Code ```python def save_config(config): """Save configuration to file.""" vault_path = normalize_path(config.get("vault_path", "")) subfolder = (config.get("subfolder", "") or "").strip().strip("/") config["configured"] = True config["configured_at"] = datetime.now().isoformat() config["vault_path"] = vault_path config["subfolder"] = subfolder # Compute full path if subfolder: full_path = os.path.join(vault_path, subfolder) else: full_path = vault_path config["full_path"] = normalize_path(full_path) # Ensure directory exists os.makedirs(config["full_path"], exist_ok=True) ``` The CLI only verifies that the supplied root is an existing directory: ```python normalized_vault_path = normalize_path(vault_path) if not verify_path_exists(normalized_vault_path): print("Vault path does not exist or is inaccessible") sys.exit(2) ``` ### Technical Analysis The subfolder is stripped of leading and trailing forward slashes but is not checked for traversal components such as `..`. After joining and normalization, the code does not verify that `full_path` remains a descendant of `vault_path`. For example, a subfolder such as `../../outside` can normalize to a location outside the selected Vault. The code then creates that directory and persists it as the article destination. In addition, the `set` action invokes `verify_path_exists`, which only checks whether the path is a directory. Although `is_obsidian_vault` exists, it is not used during configuration. Therefore, any existing directory can be selected as the purported Vault. ### Attack Path 1. A crafted configuration request supplies a legitimate Vault path and a traversal subfolder such as `../../outside`. 2. `os.path.join(vault_ ...[truncated 773 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject absolute subfolder paths. 2. Reject `.` and `..` path components. 3. Resolve both the Vault and destination to canonical paths. 4. Enforce containment before creating directories: ```python vault = os.path.realpath(vault_path) destination = os.path.realpath(os.path.join(vault, subfolder)) if os.path.commonpath([vault, destination]) != vault: raise ValueError("Subfolder must remain inside the configured Vault") ``` 5. Check containment again after directory creation to address symbolic-link changes. 6. Use `is_obsidian_vault` during the `set` action rather than merely checking for an existing directory. 7. Reject symbolic-link destinations where strict Vault confinement is required. 8. Store only a validated relative subfolder in the configuration file. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/save_article.py:71
Finding
Unsafe String Interpolation Allows YAML Frontmatter Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/save_article.py:71-80` **Vulnerability Type**: YAML metadata injection through unescaped multiline values **Risk Level**: Medium ### Vulnerable Code ```python def build_note_content(title, url, page_comment, article_markdown): created = datetime.now().strftime("%Y/%m/%d") frontmatter = "\n".join([ "---", f'title: "{title.replace(chr(34), chr(39))}"', f'url: "{url}"', f'created: "{created}"', f'pagecomment: "{(page_comment or "").replace(chr(34), chr(39))}"', "---", "", ]) ``` ### Technical Analysis The frontmatter is assembled through direct string interpolation rather than a YAML serializer. Replacing double quotes with single quotes does not safely encode YAML. The following attacker-controlled or externally controlled values can contain line breaks and YAML syntax: - `title`, derived from remote Markdown or HTML - `url`, supplied by the user - `page_comment`, supplied by the user A newline can terminate the intended scalar and inject additional YAML fields. A line containing `---` can prematurely close the frontmatter block. Backslashes and other YAML-significant characters are also not encoded consistently. Because titles originate from untrusted webpages, exploitation may occur even when the user supplies only an ordinary-looking URL. ### Attack Path 1. An attacker controls a webpage title or convinces a user to provide a crafted URL or page comment containing newline characters. 2. `build_note_content` inserts the value directly into a quoted YAML line. 3. The newline starts a new frontmatter line. 4. Injected keys or a closing `---` marker alter the resulting note structure. 5. Obsidian or installed plugins parse the injected metadata as trusted note properties. ### Impact Assessment An attacker can manipulate metadata in the generated note and potentially influence downstream Obsidian workflows or plugins that act ...[truncated 249 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Construct the metadata as a dictionary and serialize it with a maintained YAML library using safe dumping. 2. Ensure the serializer quotes or escapes multiline strings correctly. 3. If an external YAML dependency is undesirable, reject carriage returns and line feeds in single-line metadata fields. 4. Normalize control characters in titles, URLs, and comments. 5. Validate URLs before placing them in metadata. 6. Add tests covering: - Embedded newlines - `---` document delimiters - Quotes and backslashes - Unicode control characters - Multiline comments 7. Keep the visible note comment separate from frontmatter if multiline comments are required. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/download_images.py:268
Finding
Unbounded Response Reads Permit Memory and Disk Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `scripts/download_images.py:268-269, 369-370, 383-384` **Vulnerability Type**: Uncontrolled network resource consumption **Risk Level**: Medium ### Vulnerable Code ```python with urllib.request.urlopen(req, timeout=timeout) as response: content = response.read() content_type = response.headers.get('Content-Type', '') ``` ```python with urllib.request.urlopen(req, timeout=30) as response: html_content = response.read().decode('utf-8', errors='ignore') ``` ```python with urllib.request.urlopen(req, timeout=30) as response: markdown_content = response.read().decode('utf-8') ``` Downloaded image content is subsequently written in full: ```python with open(filepath, 'wb') as f: f.write(content) ``` ### Technical Analysis The code uses `response.read()` without a maximum byte count for article HTML, Jina Markdown, and every downloaded image. Network timeouts constrain elapsed waiting time but do not constrain response size. There are also no limits on: - The number of image URLs processed - Individual image size - Aggregate image size - Article HTML size - Jina response size - Available disk space - Browser-rendered document size An attacker-controlled server can stream a large response or expose many large image resources. The process may consume excessive memory before data is written to disk because each response is loaded completely into memory. ### Attack Path 1. An attacker supplies a webpage under their control. 2. The page returns an oversized HTML response, an oversized image, or references many large images. 3. The Skill reads each response into memory without a byte limit. 4. Image data is written to the Vault without an aggregate storage quota. 5. Memory or disk resources are exhausted, causing the Skill, Agent, or other local applications to fail. ### Impact Assessment Exploitation can cause denial of service for the Agent process and may consume storage available to ...[truncated 251 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Stream responses in fixed-size chunks instead of using unrestricted `read()`. 2. Enforce separate limits for: - Article HTML - Jina Markdown - Individual images - Image count - Aggregate downloaded image bytes 3. Reject responses whose declared `Content-Length` exceeds the applicable limit. 4. Continue counting bytes while streaming because `Content-Length` can be absent or false. 5. Delete partial files when a limit is exceeded. 6. Validate available disk space before downloads. 7. Stop image extraction after a reasonable maximum count. 8. Configure browser navigation and document extraction limits where supported. 9. Return a clear partial-success result when content is skipped because of resource limits. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (15)

Lp1

High
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The script performs outbound network access to arbitrary user-supplied article URLs, downloads remote images, and also contacts an external service. If the skill's declared permissions do not include network access, this creates a capability mismatch that can expose users to unexpected data exfiltration, SSRF against internal resources, or policy bypass. In the context of a skill advertised as saving web articles locally, network use to fetch the page is expected, but undeclared network capability is still security-relevant because users and permission systems cannot accurately assess what the skill does.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The README states that the primary fetch path sends target URLs to `https://r.jina.ai/<URL>`, which means user-requested pages are disclosed to a third-party service. Because this behavior is documented without a clear privacy warning, consent model, or explanation of what metadata may be exposed, users may unknowingly leak sensitive internal, private, or identifying links during normal use.

Session Persistence

Medium
Category
Rogue Agent
Content
description: 将网页文章保存到本地 Obsidian Vault,支持图片抓取、Markdown 转换、YAML frontmatter 和用户笔记附加。
allowed-tools:
  - Read
  - Write
  - Edit
  - exec
---
Confidence
85% confidence
Finding
The combination of Write, Edit, and exec permissions gives the skill durable filesystem access and the ability to invoke local scripts. Although necessary for saving articles, this enlarges the attack surface if untrusted input such as user-supplied paths, URLs, or note text is passed into scripts or used to determine filesystem targets without strong validation.

Session Persistence

Medium
Category
Rogue Agent
Content
description: 将网页文章保存到本地 Obsidian Vault,支持图片抓取、Markdown 转换、YAML frontmatter 和用户笔记附加。
allowed-tools:
  - Read
  - Write
  - Edit
  - exec
---
Confidence
85% confidence
Finding
The combination of Write, Edit, and exec permissions gives the skill durable filesystem access and the ability to invoke local scripts. Although necessary for saving articles, this enlarges the attack surface if untrusted input such as user-supplied paths, URLs, or note text is passed into scripts or used to determine filesystem targets without strong validation.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The skill defaults to fetching article content through Jina.ai but does not clearly disclose to users that submitted URLs and potentially retrieved page content are sent to a third-party service. This is a privacy and data-handling risk, especially when users save internal, private, or sensitive links under the assumption that processing is local.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The module reads Obsidian's application config and recursively scans broad user directories such as Documents, Desktop, and Mobile Documents to discover vaults. For a skill whose stated purpose is saving content to a user-configured vault, this is excessive filesystem enumeration that exposes private directory structure and application state, increasing privacy risk and collecting more local metadata than necessary.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
This code prints user-facing status text in Chinese for the CLI path, including success, error, and configuration messages. The file provides no language selection, fallback, or documentation that the tool is intentionally Chinese-only, which creates a natural-language locale policy concern.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The set and clear command flows emit user-visible messages only in Chinese, again without any opt-in or documented regional restriction. Because these are primary interaction paths, the forced locale is a consistent policy issue rather than an isolated string choice.

Description-Behavior Mismatch

Medium
Confidence
99% confidence
Finding
The code sends the user-provided article URL to https://r.jina.ai for markdown extraction, which means a third party learns what the user is saving and may fetch/process the target content externally. This contradicts the local-save expectation in the skill description and creates a privacy/data-handling issue, especially if users save private, sensitive, intranet, or tokenized URLs. The skill context makes this more dangerous because users are likely to assume content stays local except for the original site being fetched.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
The script fetches article content and downloads images via process_article(url, output_dir), which implies network access and data transmission based on a user-supplied URL. While the module docstring mentions fetching and downloading, there is no runtime confirmation, user-facing notice, or explicit warning near execution that external network requests will be made.

Natural-Language Policy Violations

Low
Confidence
74% confidence
Finding
The trigger examples are exclusively in Chinese, including operational commands such as saving and reconfiguration, but the README does not say whether other languages are supported or whether Chinese is required. This can amount to an implicit language constraint without user opt-in or documented justification.

Natural-Language Policy Violations

Low
Confidence
86% confidence
Finding
The manifest description and the entire user-facing instruction set are presented in Chinese, including trigger phrases and setup prompts, with no indication that other languages are supported or that Chinese is optional. This can amount to a language/locale policy issue when the skill effectively forces a specific language without opt-in.

Context-Inappropriate Capability

Low
Confidence
96% confidence
Finding
The setup flow injects promotional content for a Chrome extension and an unrelated community during a task that is supposed to configure article saving to Obsidian. This creates an unnecessary trust and social-engineering surface, because users may be nudged toward external products or communities without a security or functional need tied to the requested action.

Context-Inappropriate Capability

Low
Confidence
77% confidence
Finding
The stated purpose is saving webpage articles to a local Obsidian vault with images and markdown conversion. While fetching webpages is expected, embedding a full browser automation stack to render pages is a materially broader capability than the manifest describes, especially since it can execute page scripts and access dynamically rendered content.

Missing User Warnings

Low
Confidence
80% confidence
Finding
This code writes a generated markdown note to disk, which changes user data in the specified output directory. Although the module docstring says it writes an Obsidian note, the execution path provides no print/log message or confirmation indicating the exact file write that is about to occur.

Static analysis

No suspicious patterns detected.