Back to skill

Security audit

Obsidian Sync KB

Security checks for vulnerabilities and agentic risk

Overview

This appears to be a real Obsidian knowledge-base tool, but it needs review because it sends note-derived data to the web by default and can fetch arbitrary URLs found in notes.

Review this skill before installing if your Obsidian vault contains private, client, work, or internal research notes. Use --disable-network or set research.enable_network to false unless you intentionally want the tool to contact note-linked sites and DuckDuckGo, and avoid running network enrichment on untrusted or sensitive synced notes.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/kb_tool.py:1460
Finding
Server-Side Request Forgery Through Unrestricted Note-Controlled URL Fetching<![CDATA[ ## Vulnerability Details **File Location**: `scripts/kb_tool.py:1460-1472`, `scripts/kb_tool.py:1608-1630`, and `scripts/kb_tool.py:1725-1789` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code The network-fetching function accepts an arbitrary URL and passes it directly to `urllib.request.urlopen`: ```python def fetch_web_document(url: str, config: Config) -> Dict[str, Any]: if not config.enable_network: return {"status": "failed", "url": url, "text": "", "error": "network disabled"} request = urllib.request.Request( url, headers={ "User-Agent": DEFAULT_USER_AGENT, "Accept": "text/html,application/xhtml+xml,text/plain,application/json;q=0.8,*/*;q=0.5", }, ) try: with urllib.request.urlopen(request, timeout=config.network_timeout) as response: raw = response.read(250000) ``` HTTP URLs extracted from notes become source candidates without destination validation: ```python def source_candidates_for_note(note: NormalizedNote, vault_root: pathlib.Path) -> List[Dict[str, str]]: candidates: List[Dict[str, str]] = [] seen = set() for link in dedupe_keep_order(([note.original_url] if note.original_url else []) + note.source_links): if not link or link in seen: continue seen.add(link) if link.startswith("obsidian://"): resolved = resolve_obsidian_uri_to_path(link, vault_root) if resolved: candidates.append({"kind": "obsidian_note", "value": str(resolved.resolve()), "raw": link}) else: candidates.append({"kind": "obsidian_note", "value": "", "raw": link}) elif link.startswith("http"): kind = "source_url" if link == note.original_url else "embedded_url" candidates.append({"kind": kind, "value": link, "raw": link}) return candidates ``` The resulting candidate is fetched ...[truncated 3881 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Default to no network access** - Change `research.enable_network` to `false`. - Require explicit user opt-in for each build or trusted vault. 2. **Restrict protocols** - Permit HTTPS only unless HTTP is explicitly required and approved. - Reject URLs containing user information or unsupported schemes. 3. **Validate resolved destinations** - Resolve all hostnames before connecting. - Reject IPv4 and IPv6 loopback, private, link-local, multicast, unspecified, and reserved ranges. - Revalidate immediately before connection to reduce DNS-rebinding risk. 4. **Validate redirects** - Disable automatic redirects or implement a redirect handler that validates every target. - Apply the same scheme, hostname, IP-range, and port policy to each redirect. - Enforce a small redirect limit. 5. **Restrict ports and domains** - Allow only ports 443 and, if strictly necessary, 80. - Prefer an explicit trusted-domain allowlist. - Require confirmation before contacting a domain first observed in note content. 6. **Isolate network enrichment** - Run fetching in a sandbox or network namespace without access to localhost, private networks, or metadata services. - Use an egress proxy with destination filtering. 7. **Limit retained response data** - Avoid persisting raw internal responses. - Record only approved excerpts after destination and content validation. - Clearly mark the provenance of remotely fetched content. 8. **Add regression tests** - Verify rejection of `127.0.0.1`, `::1`, RFC 1918 addresses, link-local ranges, integer/encoded IP representations, and redirects to prohibited destinations. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/kb_tool.py:1548
Finding
Private Note Content Can Be Disclosed to a Public Search Provider by Default<![CDATA[ ## Vulnerability Details **File Location**: `scripts/kb_tool.py:1548-1557`, `scripts/kb_tool.py:1587-1599`, `scripts/kb_tool.py:1768-1774`, `scripts/config.yaml:14-19`, and `scripts/setup_config.py:21-24` **Vulnerability Type**: Unintended transmission of sensitive note-derived data **Risk Level**: Medium ### Vulnerable Code The public-search function places the supplied query into a DuckDuckGo request: ```python def search_public_sources(query: str, preferred_domain: str, config: Config) -> List[str]: if not config.enable_network or not query.strip(): return [] final_query = normalize_whitespace(query) if preferred_domain: final_query = f"{final_query} site:{preferred_domain}" url = "https://duckduckgo.com/html/?" + urllib.parse.urlencode({"q": final_query}) result = fetch_web_document(url, config) ``` The search query may be derived directly from the note title, summary, or body: ```python def build_search_query(note: NormalizedNote) -> str: title = normalize_whitespace(strip_markdown_links(note.title)) normalized_title = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff]+", "", title).lower() generic = {"docs", "docs2", "消息", "内容创作", "message", "opencla", "claudecod"} if normalized_title and normalized_title not in generic and len(normalized_title) >= 5: return title if len(note.summary) >= 10: return note.summary[:80] if len(note.clean_text) >= 20: return build_summary(note.clean_text, max_chars=80) return "" ``` The query is sent when enrichment has insufficient local text: ```python combined_text = combine_texts_for_summary(note, evidence) if config.enable_network and len(combined_text) < config.research_min_chars: search_query = build_search_query(note) if search_query: for url in search_public_sources(search_query, note.source_domain, config): ``` Network access is enabled by default in the distributed configur ...[truncated 3573 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Disable network enrichment by default** - Set `research.enable_network: false`. - Change `parser.set_defaults(enable_network=False)`. - Require an explicit `--enable-network` option. 2. **Separate source fetching from public search** - Provide independent settings such as `enable_source_fetching` and `enable_public_search`. - Do not infer consent to public search merely from consent to fetch a note's explicit source URL. 3. **Require informed consent** - Display the exact query and destination before transmission. - Require confirmation for each query or each batch. - Clearly document that titles, summaries, or note excerpts may be sent externally. 4. **Apply sensitivity controls** - Allow users to mark folders or notes as private and permanently exclude them from external search. - Detect and redact likely credentials, email addresses, customer identifiers, internal hostnames, tokens, and other sensitive patterns. - Never send raw body-derived text without explicit approval. 5. **Use privacy-preserving query generation** - Prefer manually supplied generic keywords. - Reduce queries to non-sensitive topic tags rather than raw titles or summaries. - Provide a local-only mode that never contacts third parties. 6. **Avoid sensitive URL logging** - Where practical, send search terms in a request body to a trusted service rather than in a URL. - Ensure application and proxy logs do not persist query text. - This is defense in depth and does not replace consent or minimization. 7. **Add auditability** - Log locally which note caused each external request, what redacted query was sent, and which destination received it. - Provide a dry-run mode listing all proposed outbound requests. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (19)

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/kb_tool.py build-index
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
The skill is described as a local Obsidian knowledge-base builder, but the code performs outbound HTTP fetching and search-engine based enrichment. That expands trust boundaries significantly: local note titles, summaries, URLs, and derived query text can be transmitted to third parties, which can leak sensitive research content and violate user expectations about a purely local indexing tool.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill invokes local scripts that read environment variables, read/write files, and may use network access, but it does not declare any explicit tool scope or permission boundaries. This can lead to overbroad agent execution where the agent may access vault contents or external resources without clear least-privilege constraints, increasing the risk of unintended data exposure or unsafe operations.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The behavior section mandates that responses return `简要结论`, `相关文章`, `关键摘录`, and `引用来源`, which imposes a specific language/locale format. The file does not indicate this is optional, user-selected, or justified as a region-specific requirement, so it conflicts with the language/locale policy criteria.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The natural-language default prompt includes Chinese text ('笔记同步助手') as part of the required instruction, which imposes a specific language/locale behavior. The file does not indicate user opt-in, alternative language support, or a documented region-specific reason for this constraint.

Context Window Stuffing

Medium
Category
Memory Poisoning
Content
clean_text, image_refs_json, source_links_json, auto_topics_json, summary, quality_score,
                theme_candidate, change_summary, content_hash, last_seen_at, last_enriched_at, retry_after,
                obsidian_path, raw_text_length
            ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            [
                (
Confidence
80% confidence
Finding
Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The code submits derived search queries to DuckDuckGo for public source discovery, even though the stated purpose is structuring synced Obsidian notes. Those queries are built from note titles, summaries, or cleaned text, so confidential note content may be exposed to an external search provider without an obvious necessity or informed consent.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
Network fetching and public search use note-derived content to form outbound requests, but this file provides no user-facing warning, consent flow, or audit disclosure. In a knowledge-base tool processing personal or organizational notes, silent transmission of derived content to external services creates a privacy and data-governance risk even if the request body is only a search query.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
CLI descriptions, generated markdown sections, status messages, and topic labels are consistently emitted in Chinese throughout the file. Because the skill does not provide language or locale selection, this is a natural-language policy concern under the locale-choice rule.

Natural-Language Policy Violations

Low
Confidence
86% confidence
Finding
The configuration uses Chinese directory names in multiple path values, which imposes a specific language/locale choice in a natural-language-facing part of the skill. There is no indication in this file that the locale is optional, user-selectable, or justified as region-specific.

Intent-Code Divergence

Low
Confidence
75% confidence
Finding
The comment says `fetch_web_document` strips HTML tags, so a separate raw download is needed, but immediately above the code stores `result.get("text", "")` into `raw_html`, which is not raw HTML at all. This is an intent/documentation contradiction that can mislead maintainers about what data is available at this point and why the second network request exists.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The script hard-codes Chinese default folder names in user-facing CLI arguments, which imposes a specific language/locale convention on all users. There is no natural-language option or opt-in that lets users choose localized defaults, so this may violate language/locale policy requirements.

Static analysis

No suspicious patterns detected.