Back to skill

Security audit

web-to-obsidian

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it says at a high level, but it fetches arbitrary URLs through third-party services with weak transport security and insufficient user warning.

Install only if you are comfortable sending article URLs to third-party conversion services and importing transformed content into Obsidian. Avoid using this with private, internal, signed, tokenized, or sensitive URLs until it validates destinations, restores normal TLS verification, uses safe temporary files, and asks before translation and external URL forwarding.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch.py:13
Finding
TLS Certificate Verification Disabled for All HTTPS Requests<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch.py:13-16`; `scripts/smart-url.py:13-16` **Vulnerability Type**: Improper certificate validation **Risk Level**: High ### Vulnerable Code `scripts/fetch.py:13-16`: ```python # Ignore SSL verification ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE ``` `scripts/smart-url.py:13-16`: ```python # Ignore SSL verification (required by some services) ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE ``` The unverified context is subsequently passed to `urllib.request.urlopen`, including in `scripts/fetch.py:98` and `scripts/smart-url.py:55`. ### Technical Analysis Both scripts explicitly disable certificate-chain validation and hostname verification. Consequently, HTTPS provides encryption without reliable endpoint authentication. Any certificate—including a self-signed certificate or one issued for a different hostname—will be accepted. An attacker capable of intercepting network traffic can impersonate the requested website or one of the external conversion services and return arbitrary content. The returned content is treated as a successfully fetched article and may later be written into an Obsidian note. Certificate verification is disabled globally for all requests made with this context rather than being limited to a narrowly scoped compatibility exception. ### Attack Path 1. A user invokes the Skill to retrieve an HTTPS article. 2. The script connects to a conversion service or directly to the original website. 3. An attacker with an on-path position intercepts the connection. 4. The attacker presents an arbitrary or self-signed certificate. 5. The script accepts the certificate because hostname and certificate verification are disabled. 6. The attacker supplies modified Markdown or HTML. 7. The forged content is returned as successful output and ...[truncated 541 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove both insecure SSL overrides: - `ssl_context.check_hostname = False` - `ssl_context.verify_mode = ssl.CERT_NONE` - Use Python’s default verified TLS context: ```python ssl_context = ssl.create_default_context() ``` - Do not silently fall back to unverified TLS when certificate validation fails. - Return a clear error identifying the affected service and certificate problem. - Ensure the runtime has a current CA trust store. - If private certificate authorities must be supported, load only the explicitly trusted CA certificate with `ssl.create_default_context(cafile=...)`. - Consider enforcing a minimum TLS version appropriate for the deployment environment. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch.py:66
Finding
User-Controlled URL Fetching Permits SSRF and Unsafe Redirects<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch.py:66-68`, `scripts/fetch.py:88-103`; `scripts/smart-url.py:42-57` **Vulnerability Type**: Server-Side Request Forgery **Risk Level**: High ### Vulnerable Code `scripts/fetch.py:66-68` directly falls back to the user-supplied URL: ```python # All cleaning services failed; attempt direct retrieval of original content try: result = fetch_url(original_url, timeout) ``` `scripts/fetch.py:88-103` performs the request without validating its scheme, host, port, resolved address, or redirect destination: ```python def fetch_url(url: str, timeout: int = 30) -> dict: """Retrieve URL content""" req = urllib.request.Request( url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.0' } ) with urllib.request.urlopen(req, timeout=timeout, context=ssl_context) as response: content = response.read().decode('utf-8', errors='ignore') return { "success": response.status == 200, "content": content, "status": response.status } ``` `scripts/smart-url.py:42-57` also probes constructed URLs without validating redirect destinations: ```python def test_url(url: str, timeout: int = 10) -> bool: """Test whether URL is available""" try: req = urllib.request.Request( url, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.0' }, method='HEAD' ) with urllib.request.urlopen(req, timeout=timeout, context=ssl_context) as response: return response.status == 200 except Exception: return False ``` ### Technical Analysis The scripts accept arbitrary URL strings and issue network requests without an allowlist or address-range validation. In particular, `fetch.py` directly requests the original user input after conversion servi ...[truncated 2075 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Implement centralized URL validation before every request: 1. Parse URLs with `urllib.parse.urlsplit`. 2. Allow only `http` and `https`. 3. Reject missing hostnames, URL credentials, malformed ports, and nonstandard ports unless explicitly required. 4. Resolve the hostname and reject every address in loopback, private, link-local, multicast, reserved, or unspecified ranges. 5. Explicitly block known metadata endpoints, including link-local metadata addresses. 6. Disable automatic redirects or implement a redirect handler that applies the same validation to every destination. 7. Protect against DNS rebinding by connecting only to the validated resolved address while preserving the intended hostname for TLS verification. 8. Apply outbound firewall or proxy restrictions as defense in depth. 9. Limit response size, connection time, and redirect count. 10. Consider removing direct-fetch fallback unless it is explicitly enabled by the user. Example validation logic should use the `ipaddress` module and reject a destination when any resolved address is unsafe. ]]>

other

Warning
Location
scripts/fetch.py:34
Finding
Complete Source URLs Are Disclosed to Third-Party Conversion Services<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch.py:34-38`; `scripts/smart-url.py:24-39` **Vulnerability Type**: Unannounced third-party URL disclosure **Risk Level**: Medium ### Vulnerable Code `scripts/fetch.py:34-38`: ```python # Cleaning service list (in priority order) services = [ ("jina", lambda u: f"https://r.jina.ai/http://{u.replace('https://', '').replace('http://', '')}"), ("markdown-new", lambda u: f"https://markdown.new/{u}"), ("defuddle", lambda u: f"https://defuddle.md/{u}"), ] ``` `scripts/smart-url.py:24-39`: ```python encoded_url = urllib.parse.quote(original_url, safe='') # 1. Preferred: Jina Reader (free, no API key required) jina_url = f"https://r.jina.ai/http://{original_url.replace('https://', '').replace('http://', '')}" if test_url(jina_url): return jina_url # 2. Fallback: markdown.new markdown_new_url = f"https://markdown.new/{original_url}" if test_url(markdown_new_url): return markdown_new_url # 3. Final fallback: defuddle.md defuddle_url = f"https://defuddle.md/{original_url}" return defuddle_url ``` ### Technical Analysis The complete source URL is embedded into requests sent to `r.jina.ai`, `markdown.new`, and `defuddle.md`. This can expose more than the public hostname and path. URLs may contain: - Signed query parameters. - Temporary access tokens. - Document identifiers. - Search terms. - Private hostnames and paths. - User information embedded in query strings. - URL credentials. - Fragments or other sensitive contextual data. The services may receive the URL through the request path and can record it in access logs, telemetry, caches, or error reports. The fetching workflow describes the services but does not provide a clear privacy warning or request explicit consent before transmitting the target URL. The variable `encoded_url` in `smart-url.py` is computed but not used, so it does not mitigate disclosure or ensure safe URL construction. ### Attack Path 1. A user asks t ...[truncated 1016 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Clearly disclose which third-party services receive source URLs and obtain user consent before transmission. - Provide a direct-fetch-only mode that does not contact conversion providers. - Reject URLs containing embedded username or password fields. - Remove fragments before processing. - Strip query parameters by default and require explicit approval before transmitting them. - Maintain an allowlist for query parameters known to be necessary and non-sensitive. - Warn users not to submit signed links, password-reset links, session-bearing URLs, or private document URLs. - Avoid trying multiple providers for sensitive URLs. - Add configuration that allows users or administrators to disable individual services. - Document each provider’s privacy and data-retention implications. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:90
Finding
Fixed Temporary Filename Can Cause File Overwrite or Symlink Abuse<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:90-123` **Vulnerability Type**: Unsafe temporary-file handling **Risk Level**: Medium ### Vulnerable Instructions The Skill requires generated content to be written to a fixed filename in the current directory: ```markdown Write the generated content to ***temp.md in the current directory***. ``` It then consumes and deletes that predictable path: ```bash # Method 1: Create directly using the create command obsidian create name="Article title" content="$(cat temp.md)" # Method 2: Create from a file if supported # obsidian create --file "$TEMP_FILE" name="Article title" ``` ```bash rm -f temp.md ``` ### Technical Analysis `temp.md` is a predictable, shared pathname. The workflow does not check whether the path already exists, whether it is a symbolic link, who owns it, or whether another process is using it. If an existing file named `temp.md` is present, writing the generated note can overwrite that file. If an attacker can prepare a symbolic link at that path, the write may target another file accessible to the Agent process. The subsequent `rm -f temp.md` removes the pathname without establishing that it is the temporary file created by the current operation. Concurrent Skill invocations can also interfere with one another by writing, reading, or deleting the same file. ### Attack Path 1. An attacker or another process creates `temp.md` in the expected working directory as either a valuable existing file or a symbolic link to another writable file. 2. The Agent follows the Skill instructions and writes generated article content to `temp.md`. 3. Existing data or the symbolic-link target is overwritten under the Agent’s filesystem permissions. 4. The Skill imports content from the shared path, potentially importing data written by another process. 5. The cleanup command removes `temp.md`, which may destroy an existing file or interfere with another concurrent run. A non-adversarial rac ...[truncated 591 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create a unique temporary file with `mktemp` rather than using `temp.md`: ```bash TEMP_FILE="$(mktemp "${TMPDIR:-/tmp}/web-to-obsidian.XXXXXX.md")" || exit 1 chmod 600 "$TEMP_FILE" trap 'rm -f -- "$TEMP_FILE"' EXIT ``` - Always quote the generated path: ```bash obsidian create name="$TITLE" content="$(cat -- "$TEMP_FILE")" ``` - Create temporary files atomically and refuse to follow symbolic links. - Keep restrictive permissions, such as mode `0600`. - Avoid placing temporary files in an attacker-controlled current directory. - Use a private temporary directory when multiple related files are required. - Ensure each invocation has an independent path to prevent concurrency conflicts. - If direct file input is supported, prefer passing the unique temporary path rather than expanding the entire content through command substitution. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (18)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
This second mismatch finding adds that the skill may perform undeclared connectivity checks such as HEAD requests to third-party services, while still not implementing the advertised local processing and Obsidian-save features. Hidden or undocumented network activity increases the risk of data leakage, SSRF-like misuse against internal URLs, and user deception about where their content is sent.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
This second mismatch finding adds that the skill may perform undeclared connectivity checks such as HEAD requests to third-party services, while still not implementing the advertised local processing and Obsidian-save features. Hidden or undocumented network activity increases the risk of data leakage, SSRF-like misuse against internal URLs, and user deception about where their content is sent.

Natural-Language Policy Violations

High
Confidence
95% confidence
Finding
Automatic translation of all non-Chinese content without opt-in changes the user's data and may send potentially sensitive material to external translation services or models. In this skill context, that is more dangerous because users may expect faithful archival of the original webpage into Obsidian, not automatic transformation and possible third-party disclosure.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
The code globally disables TLS certificate validation and hostname verification for all outbound HTTPS requests. This makes requests vulnerable to man-in-the-middle interception and content tampering, allowing an attacker on the network path to alter fetched content or redirect requests without detection.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill instructs network fetching of arbitrary URLs via `scripts/fetch.py` but does not declare any explicit tool scope or permissions boundary. That makes the capability less auditable and can enable unintended outbound requests, including access to attacker-chosen endpoints or privacy-sensitive links, without clear user or platform constraints.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The trigger phrases are broad enough to activate on generic requests such as saving or importing content, increasing the chance the skill runs in contexts the user did not specifically intend. Over-broad activation can lead to unexpected network fetching, translation, file writes, or data transfer to third parties without sufficiently clear consent.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The workflow explicitly instructs translation of non-Chinese content before storage, but the later file-writing instruction says the body content should not undergo any modification and should be written directly. These statements are in direct tension about whether the article body is transformed prior to saving.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill writes `temp.md` locally and later deletes it, but the user-facing guidance does not clearly warn about these filesystem operations. Silent local file creation and deletion can surprise users, interfere with existing files, and reduce auditability, especially if the current working directory is not isolated.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The skill directs the agent to run shell commands that write files, invoke an external CLI, and delete local files. Even though the stated purpose is web-to-Obsidian conversion, using unrestricted shell operations broadens the attack surface and can cause unintended filesystem changes or command misuse if values like article titles or paths are not safely handled.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The eval hard-codes that fetched content should be translated into Chinese even though the user only asked to save the article to Obsidian. This overrides user intent and can cause unauthorized transformation of source material, including loss of fidelity, licensing/compliance issues, or incorrect storage of content in a language the user did not request.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
The eval requires Chinese-language output as the default behavior without offering a language choice. While not a code-execution flaw, it encodes a policy that can systematically alter user data and mis-handle multilingual content, especially when the desired archival behavior is faithful capture rather than localization.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The eval mandates translation of English content into Chinese despite the prompt only asking to store the blog post in the user's Obsidian vault. This can lead to unrequested content rewriting, semantic drift, and reduced trust in the archived note because the stored artifact is no longer a faithful copy of the original source.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The function sends user-supplied URLs to multiple third-party content-cleaning services before attempting direct retrieval. This leaks the user's requested target to external providers and expands data exposure beyond the stated purpose of simply fetching and saving a webpage to Obsidian, which is especially risky if users submit private, internal, or sensitive URLs.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
User-provided URLs are transmitted to third-party services without any warning, consent, or indication in the skill behavior. This creates a privacy and security issue because sensitive browsing targets, internal links, or tokenized URLs may be disclosed to outside operators unexpectedly.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The function rewrites any user-supplied URL to third-party reader services (r.jina.ai, markdown.new, defuddle.md), which necessarily discloses the target URL to external providers. In a skill meant to save arbitrary web pages to Obsidian, users may supply private, internal, tokenized, or sensitive URLs, so forwarding them without explicit notice or consent creates a real privacy and data-handling risk.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The availability check performs outbound requests to constructed third-party URLs solely to test reachability, again transmitting user-derived URL data externally without user awareness. This increases exposure because even fallback probing leaks browsing targets to multiple services, and the code disables SSL verification globally, weakening transport assurances during those checks.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The top-level natural-language description and comments present the tool in Chinese only, which can impose a language choice on users without opt-in. Under the policy criteria, forcing a specific language without user choice is a natural-language policy concern unless the locale restriction is explicitly justified.

Natural-Language Policy Violations

Low
Confidence
78% confidence
Finding
The natural-language documentation strings are presented in Chinese only, with no indication that the skill is region-specific or that users may choose another language. This can conflict with language/locale policy expectations when no opt-in or justification is provided.

Static analysis

No suspicious patterns detected.