Back to skill

Security audit

Obsidian Librarian

Security checks for vulnerabilities and agentic risk

Overview

The skill broadly matches its Obsidian knowledge-base purpose, but it can send private notes or files to external services and has path-scope flaws that could read or delete files outside the intended vault inbox.

Review before installing. Use this only with vaults and URLs you are comfortable sending to Gemini, Apify, Brave search fallback, and optionally Supabase. Avoid enabling Supabase until the endpoint is HTTPS/trusted and the schema has real per-user access controls. Patch or constrain --inbox-file and vault symlink handling before using it on sensitive files, and prefer --keep-inbox until deletion behavior is fixed.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (6)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/run_pipeline.py:62
Finding

Arbitrary File Disclosure and Deletion Through Unrestricted Inbox Paths

Content
View full analysis
ProcessedNote: raw = RawInboxItem( file_path=file_path, raw_content=file_path.read_text(encoding="utf-8"), detected_at=datetime.now(timezone.utc), ) synthesized = self.synthesizer.synthesize(raw) if title_override: synthesized.title = title_override.strip() synthesized.markdown_body = self._override_h1(synthesized.markdown_body, synthesized.title) architected = self.architect.architect(synthesized, raw.raw_content) final_path = self.file_manager.write_note(architected, synthesized) self.vault_index.add_concept(synthesized.title, []) if not keep_inbox: self.file_manager.remove_inbox_file(file_path) ``` ```python # scripts/file_manager.py:41-45 @staticmethod def remove_inbox_file(path: Path) -> None: try: path.unlink() except FileNotFoundError: return ``` ### Technical Analysis The documented purpose of `--inbox-file` is to process a file already staged inside the configured Obsidian inbox. The implementation resolves the supplied path but never verifies that it is located under `settings.inbox_path`. The pipeline consequently accepts any filesystem path readable by the process. It reads the complete file, sends its content through the Gemini synthesis and metadata pipeline, writes a derived copy into the vault, and then deletes the original file unless `--keep-inbox` is specified. Calling `Path.resolve()` normalizes ...[truncated 1370 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/vault_reader.py:144
Finding

Vault Symlinks Can Cause Files Outside the Vault to Be Indexed and Exfiltrated

Content
View full analysis
list[VaultChunk]: try: text = file_path.read_text(encoding="utf-8") except Exception: return [] ``` ```python # scripts/vault_reader.py:144-150 def read_all_vault_files(vault_path: Path, inbox_folder: str = "_Inbox") -> list[VaultChunk]: all_chunks: list[VaultChunk] = [] for md_path in vault_path.rglob("*.md"): if ".obsidian" in md_path.parts or inbox_folder in md_path.parts: continue chunks = read_vault_file(md_path, vault_path) all_chunks.extend(chunks) ``` ```python # scripts/vault_embedder.py:27-43 for i in range(0, len(chunks), BATCH_SIZE): batch = chunks[i : i + BATCH_SIZE] texts = [c.content for c in batch] embeddings = embed_texts( api_key=settings.gemini_api_key, texts=texts, model=settings.embedding_model, dimensions=settings.embedding_dimensions, ) rows = [ _chunk_to_row(chunk, emb) for chunk, emb in zip(batch, embeddings) ] upsert_vault_chunks(settings, rows) ``` ```python # scripts/vault_index.py:15-20 def build(self) -> "VaultIndex": self._concepts.clear() for path in self.vault_path.rglob("*.md"): if ".obsidian" in path.parts or self.inbox_folder in path.parts: continue title, aliases = self._extract_concepts_from_file(path) self.add_concept(title, aliases) ``` ### Technical Analysis The recursive scans trust paths found beneath the vault syntactically but do not reject symbolic links or verify that each resolved target remains inside the resolved ...[truncated 1726 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/common.py:77
Finding

Gemini and Apify API Credentials Are Embedded in URLs and Leaked Through Error Messages

Content
View full analysis
str: title = self._fallback_title(raw) reason = str(error) if error else "unknown error" body = raw.raw_content.strip() or "No content was captured." return ( f"# {title}\n ...[truncated 2014 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/supabase_client.py:9
Finding

Unvalidated Supabase Endpoint Can Receive Credentials and Plaintext Vault Content

Content
View full analysis
None: if not url or not key: raise RuntimeError("SUPABASE_URL and SUPABASE_KEY must be set") self.base_url = url.rstrip("/") self.key = key self._headers = { "apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json", "Prefer": "return=minimal", } ``` ```python # scripts/rag_backend.py:157-164 def upsert_vault_chunks(settings: LibrarianSettings, rows: list[dict[str, Any]]) -> None: if settings.use_supabase_rag: _assert_supabase_compatible(settings) client = SupabaseClient(settings.supabase_url, settings.supabase_key) client.upsert("vault_chunks", rows, on_conflict="file_path,chunk_index") return ``` ### Technical Analysis `SUPABASE_URL` is accepted directly from the environment without checking its scheme, hostname, port, embedded credentials, or trust relationship. The Supabase client sends both the API key and bearer token to that endpoint. The uploaded rows include the relative file path, complete plaintext vault chunk, embedding, and metadata. An HTTP endpoint permits network interception, while an attacker-controlled HTTPS endpoint directly receives both the Sup ...[truncated 1368 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
sql/vault_chunks.sql:5
Finding

Supabase Schema Lacks Row-Level Security and Per-User Data Isolation

Content
View full analysis
query_embedding) as similarity from vault_chunks vc where 1 - (vc.embedding <=> query_embedding) > match_threshold and (filter_category is null or vc.metadata->>'category' = filter_category) order by vc.embedding <=> query_embedding limit match_count; end; $$; ``` ### Technical Analysis The supplied schema stores private note content but does not: - Enable or force row-level security. - Define ownership columns. - Create owner-scoped access policies. - Restrict the search RPC to the authenticated owner. - Revoke default table or function access. - Separate data belonging to multiple users or vaults. The `match_vault_chunks` function searches the complete table. Its only optional filter is a c ...[truncated 1278 chars]
Remediation
View remediation

T01 · Skill Instruction Hijacking

Warning
Location
scripts/prompts.py:3
Finding

Untrusted Web and Vault Content Is Inserted Into Model Prompts Without Strong Injection Boundaries

Content
View full analysis
Remediation
View remediation
... ``` 4. Prefer API-supported structured message parts rather than interpolating all data into one instruction-bearing string. 5. Scan imported content for common injection indicators and flag affected notes for review. 6. Validate generated Markdown before persistence, including unexpected links, hidden HTML, instruction-like text, and deviations from the requested structure. 7. For RAG, separate retrieved quotations from answer-generation directives and require claim-to-source verification. 8. Consider showing the user a preview and requiring confirmation before persisting content from untrusted web sources. 9. Record provenance and trust level in note metadata so untrusted material can be excluded or down-ranked during future retrieval. ]]>
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
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (23)

Tainted flow: 'request' from os.environ.get (line 350, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/common.py (reported line 79)May include surrounding context.

python
request = urllib.request.Request(url, data=data, method=method, headers=request_headers)
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            return response.read()
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")

Tainted flow: 'request' from os.environ.get (line 350, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
91% confidence
Finding

The redirect resolver performs server-side requests to user-supplied URLs on shortener domains, which creates an SSRF primitive. An attacker could cause the agent to make network requests to attacker-chosen destinations, including internal or link-local endpoints if DNS or redirects are manipulated.

Content

Scanner excerpt · scripts/common.py (reported line 351)May include surrounding context.

python
for _ in range(max_hops):
        try:
            request = urllib.request.Request(current, method="HEAD", headers={"User-Agent": "Mozilla/5.0"})
            with urllib.request.urlopen(request, timeout=timeout) as response:
                final = response.geturl()
        except Exception:
            try:

Tainted flow: 'request' from os.environ.get (line 355, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
91% confidence
Finding

This GET fallback in redirect resolution preserves the same SSRF risk as the HEAD request path, but may increase impact because it can fetch full content from attacker-chosen endpoints. In a skill that ingests arbitrary user-provided URLs, this broadens the reachable attack surface significantly.

Content

Scanner excerpt · scripts/common.py (reported line 356)May include surrounding context.

python
except Exception:
            try:
                request = urllib.request.Request(current, headers={"User-Agent": "Mozilla/5.0"})
                with urllib.request.urlopen(request, timeout=timeout) as response:
                    final = response.geturl()
            except Exception:
                return current

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The declared description presents a broad capture-and-query Obsidian skill with web ingestion, AI processing, and retrieval capabilities. The supplied code chunk does not implement any of those primary functions. Instead, it only scans local markdown files and extracts concept names and aliases from filenames/frontmatter to build an in-memory index. This is related to vault indexing, but it is only a small supporting component and does not accurately represent the overall declared behavior for this chunk. Therefore, the description materially overstates what the code actually does.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding

The skill advertises and instructs use of shell, network, file read/write, and environment-dependent operations but does not declare an explicit tool scope such as permissions or allowed-tools. That creates an authorization and review gap: operators and users cannot easily constrain what the skill may access, increasing the risk of unintended file modification, secret use, or network egress during execution.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The broad trigger phrase "research capture" can activate the skill for ordinary conversational requests that were not intended to save data or invoke external processing. In this skill's context, accidental activation is more dangerous because it may cause content to be written to the user's vault and sent to third-party services for fetching or model processing.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The instruction to prefer the Obsidian vault when save intent is ambiguous creates an overly broad activation rule that can reroute generic save requests into this skill. That increases the chance of unintended persistence of user content and downstream disclosure to Gemini/Apify, especially when the user may have meant a local, transient, or different storage action.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The skill describes URL ingestion and model-based processing but does not clearly warn users that fetched content and note contents may be transmitted to third-party services such as Apify, Gemini, and optionally Supabase. In a knowledge-base skill, this is particularly risky because users may save sensitive research notes, credentials, personal data, or proprietary material without understanding the external data exposure.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

This code sends user-derived note content (content.markdown_body) and up to 500 vault concepts to an external Gemini API, which can expose sensitive personal knowledge-base data to a third party. In an Obsidian 'second-brain' skill, vault contents are especially likely to contain private notes, research, credentials, or proprietary information, so undisclosed external transmission increases privacy and data-handling risk.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/common.py (reported line 20)May include surrounding context.

python
GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta"
APIFY_API_BASE = "https://api.apify.com/v2"


def ensure_parent(path: Path) -> None:

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

Falling back to an external curl binary on certificate verification failure weakens the trust model and expands the attack surface. It creates inconsistent TLS behavior across code paths and relies on ambient system configuration and PATH resolution, which can undermine guarantees expected from the standard library transport.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/common.py (reported line 120)May include surrounding context.

python
body_bytes = json.dumps(json_body).encode("utf-8")
        command.extend(["--data-binary", "@-"])

    result = subprocess.run(
        command,
        input=body_bytes,
        capture_output=True,

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

This code sends user-provided URLs and fetched content to third-party services such as Apify and Gemini without any visible consent, warning, or minimization in this file. In a knowledge-base skill, users may submit private links, internal documents, or sensitive research targets, making undisclosed external transmission a meaningful privacy and data-handling risk.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The search fallback submits the original target URL and derived identifiers to Brave Search, disclosing user interests and potentially private or unlisted URLs to an unrelated third party. Because the skill is explicitly designed to capture arbitrary URLs, this behavior is more dangerous than in a generic browsing tool and can leak sensitive research activity.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

This code constructs a remote API request containing the user's text content in the POST body and the API key in the request URL, then sends it via http_json_request. There is no confirmation prompt, logging, comment, or docstring warning that text and credentials are being sent to an external service.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

This code file includes a destructive operation via path.unlink() that deletes a file, but the function contains no confirmation prompt, logging, print statement, or explanatory comment/docstring warning that deletion occurs. Under the code-file criteria, destructive file deletion should have some visible disclosure unless clearly documented elsewhere, which is not present in this file.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

When keep_inbox is false, the pipeline removes the original inbox file after processing. This is a potentially destructive file operation, and while the code logs reindexing status later, there is no visible confirmation prompt, warning comment, or user-facing disclosure here about deletion behavior.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The code sends retrieved vault context directly to gemini_generate_text, which transmits personal Obsidian note content to an external model API. Because this skill is explicitly built around a user's private knowledge base, the transferred data may include sensitive notes, credentials, personal research, or other confidential material, and this file shows no consent gate, redaction, or disclosure before exfiltration.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

This code performs a network fetch via fetch_url_source(args.url) and then writes the fetched content into the vault, but there is no visible confirmation prompt or user-facing disclosure at the point of execution. While the command help says it can ingest a URL, it does not warn that the skill will contact a remote server and transmit the supplied URL as part of that operation.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The synthesizer sends raw captured content directly to Gemini, which can include arbitrary user-supplied notes, URLs, tweets, or extracted page text. In a knowledge-base skill, that creates a real data-exfiltration/privacy risk if users are not clearly informed and given control over what content is transmitted to a third-party model provider.

Content

No source excerpt is available for this finding.

Ssd 4

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

On synthesis failure, the code embeds raw untrusted content verbatim into markdown that is likely stored and later consumed by downstream retrieval or AI workflows. That preserves prompt-injection text, malicious instructions, or hostile markdown exactly as captured, allowing latent instruction carryover into later LLM-based search, summarization, or RAG operations.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

This code recursively reads all markdown files under the provided vault path, excluding only specific folders, which can ingest large amounts of user-authored content. There is no confirmation prompt, logging, comment, or docstring warning that the function will traverse and read potentially sensitive notes from the user's vault.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
81% confidence
Finding

This code reads API and database credentials from environment variables (GEMINI_API_KEY, SUPABASE_URL, SUPABASE_KEY), which is a sensitive operation under the warning criteria for code files. There is no confirmation prompt, logging, comment, or docstring here that informs users the skill accesses these secrets.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.