Back to skill

Security audit

Book Capture Obsidian

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly purpose-aligned, but its migration and dashboard scripts can write, move, or delete files too broadly, and Google enrichment shares library metadata by default.

Review before installing. Use a backed-up or test Obsidian vault first, keep dry-run enabled until results are reviewed, disable Google enrichment for sensitive libraries, avoid untrusted Goodreads CSV files, and do not use shelf grouping or custom dashboard paths until path containment and collision handling are fixed.

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

Warning
Location
scripts/security_scan_no_pii.sh:11
Finding
Predictable temporary file enables symlink-based file truncation## Vulnerability Details **File Location**: `scripts/security_scan_no_pii.sh:11-15` **Vulnerability Type**: Unsafe predictable temporary file **Risk Level**: Medium ### Vulnerable Code ```sh if grep -RInE "$PATTERN" $TARGETS >/tmp/book_capture_security_scan.txt 2>/dev/null; then echo "Security scan failed. Potential sensitive strings found:" cat /tmp/book_capture_security_scan.txt exit 1 fi ``` ### Technical Analysis The security scan writes its results to the fixed, predictable path `/tmp/book_capture_security_scan.txt`. On a multi-user system, another local user can create this path in advance as a symbolic link to another file. Shell output redirection is performed before `grep` executes. Consequently, the linked destination is opened and truncated even when `grep` ultimately finds no matches. The operation occurs with the privileges of the account running the security scan. The script does not use exclusive temporary-file creation, verify file ownership, reject symbolic links, or remove the file reliably after execution. ### Attack Path 1. An attacker with local access predicts the fixed temporary filename. 2. The attacker creates a symbolic link: ```sh ln -s /path/to/victim-writable-file /tmp/book_capture_security_scan.txt ``` 3. A user or agent runs: ```sh sh scripts/security_scan_no_pii.sh ``` 4. The shell follows the symbolic link while processing the output redirection. 5. The linked file is truncated and may subsequently receive scan output. Exploitation is limited to files writable by the account executing the Skill; this issue does not independently grant higher operating-system privileges. ### Impact Assessment A local attacker can cause arbitrary files writable by the Skill's execution account to be truncated or overwritten with scan output. This can lead to loss of user data, corruption of configuration files, or disruption of other applications. The iss ...[truncated 83 chars]
Remediation
## Remediation Suggestions Create the temporary file atomically with `mktemp` and remove it through an exit trap: ```sh TMP_FILE="$(mktemp "${TMPDIR:-/tmp}/book_capture_security_scan.XXXXXX")" trap 'rm -f "$TMP_FILE"' EXIT HUP INT TERM if grep -RInE "$PATTERN" $TARGETS >"$TMP_FILE" 2>/dev/null; then echo "Security scan failed. Potential sensitive strings found:" cat "$TMP_FILE" exit 1 fi ``` Additional hardening measures: - Quote the temporary filename on every use. - Do not reuse a fixed path under a shared temporary directory. - Run the scan with the minimum required privileges. - Consider avoiding a temporary file entirely by capturing or piping the result safely.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate_dashboard.py:92
Finding
Dashboard output path is not restricted to the configured vault## Vulnerability Details **File Location**: `scripts/generate_dashboard.py:92-96, 149-153` **Vulnerability Type**: Arbitrary file write through path traversal or absolute path injection **Risk Level**: Medium ### Vulnerable Code ```python def generate_dashboard(vault_path: str, notes_dir: str, dashboard_file: str, template_file: str) -> Dict[str, Any]: vault = Path(vault_path).expanduser() books_root = vault / Path(notes_dir) dashboard_path = vault / Path(dashboard_file) if not books_root.exists(): return make_result(STAGE, ok=False, error=f"books directory not found: {books_root}") template_path = Path(template_file).expanduser() if not template_path.exists(): return make_result(STAGE, ok=False, error=f"dashboard template not found: {template_path}") # ... dashboard_path.parent.mkdir(parents=True, exist_ok=True) previous = dashboard_path.read_text(encoding="utf-8") if dashboard_path.exists() else None updated = previous != rendered if updated: dashboard_path.write_text(rendered, encoding="utf-8") ``` ### Technical Analysis `dashboard_file` is treated as a path relative to the vault, but this constraint is not enforced. An absolute `dashboard_file` causes `pathlib` to discard the preceding vault path. A relative value containing traversal components such as `../../target.md` can also resolve outside the vault. The resulting path is used to create parent directories and overwrite the destination without a resolved-path containment check. The value can be supplied through the `--dashboard-file` argument or the `BOOK_CAPTURE_DASHBOARD_FILE` environment variable. ### Attack Path 1. An attacker, unsafe wrapper, or compromised execution environment controls the dashboard path argument or environment variable. 2. A path outside the vault is supplied, for example: ```sh python3 scripts/generate_dashboard.py \ --vault-pa ...[truncated 726 chars]
Remediation
## Remediation Suggestions Resolve and validate the output path before performing any filesystem operation: ```python vault_root = Path(vault_path).expanduser().resolve() dashboard_relative = Path(dashboard_file) if dashboard_relative.is_absolute(): return make_result( STAGE, ok=False, error="dashboard_file must be relative to the vault", ) dashboard_path = (vault_root / dashboard_relative).resolve() if dashboard_path == vault_root or vault_root not in dashboard_path.parents: return make_result( STAGE, ok=False, error="dashboard path escapes the configured vault", ) ``` Additional hardening measures: - Reject traversal components before path construction. - Apply equivalent containment validation to `notes_dir`. - Check containment immediately before writing to reduce symlink-related race exposure. - Consider using a directory file descriptor and platform-supported no-follow operations for environments where untrusted local users can modify vault paths concurrently. - Require an explicit vault path rather than defaulting write operations to the current directory.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/migrate_goodreads_csv.py:67
Finding
Unsafe shelf grouping can relocate or delete book notes## Vulnerability Details **File Location**: `scripts/migrate_goodreads_csv.py:67-77, 385-399` **Vulnerability Type**: Path traversal and unsafe destructive collision handling **Risk Level**: Medium ### Vulnerable Code ```python def _normalize_slug(value: str) -> str: text = (value or "").strip().lower() if not text: return "unknown" for token in ("/", "\\", ",", ":"): text = text.replace(token, "-") text = "-".join(text.split()) while "--" in text: text = text.replace("--", "-") text = text.strip("-") return text or "unknown" ``` ```python if group_by_shelf: note_path = Path(str(result.get("note_path") or "")) if note_path.exists(): shelf_value = str(payload.get("shelf") or "inbox") shelf_dir = root / _shelf_subfolder(shelf_value) shelf_dir.mkdir(parents=True, exist_ok=True) target = shelf_dir / note_path.name if note_path != target: if target.exists(): # Keep existing target and remove duplicate source file try: note_path.unlink() except Exception: pass else: note_path.rename(target) moved += 1 ``` ### Technical Analysis Shelf names originate from the Goodreads CSV `Exclusive Shelf` field. `_normalize_slug()` removes a limited set of separators but does not reject `"."` or `".."` path components. For example, a shelf value of `..` remains unchanged. Joining that value with `root` produces a path resolving to the parent of the configured notes directory. No resolved-path containment validation is performed before directory creation or note movement. The collision branch introduces an additional destructive behavior: when `target` already exists, the source note is unconditionally deleted. The code does not verify that the existing destinatio ...[truncated 1327 chars]
Remediation
## Remediation Suggestions Use a strict allowlist for shelf directory names and explicitly reject path metacharacters and special path components: ```python import re def _shelf_subfolder(value: str) -> str: slug = _normalize_slug(value) if slug in {".", ".."} or not re.fullmatch(r"[a-z0-9][a-z0-9_-]*", slug): raise ValueError("invalid shelf name") return slug ``` Enforce resolved containment before creating or moving files: ```python root_resolved = root.expanduser().resolve() shelf_dir = (root_resolved / _shelf_subfolder(shelf_value)).resolve() if shelf_dir == root_resolved or root_resolved not in shelf_dir.parents: raise ValueError("shelf directory escapes notes root") ``` Replace destructive collision handling with an explicit conflict policy: - Compare canonical identity fields such as ISBN before treating files as duplicates. - Never delete the source solely because a filename exists. - Generate a unique filename or return a row-level conflict. - Preserve backups when consolidating verified duplicates. - Use atomic file operations where possible. - Report move and deletion failures instead of suppressing exceptions. - Add tests covering `"."`, `".."`, traversal attempts, symlinked directories, filename collisions, and preservation of user content.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (28)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The workflow explicitly recommends Google-based enrichment, but the skill description and permissions model do not clearly declare or constrain external network access. Undeclared outbound requests can expose user data such as ISBNs, reading inventory, or vault-derived metadata to third parties and can bypass user expectations about local-only processing.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The workflow explicitly recommends Google-based enrichment, but the skill description and permissions model do not clearly declare or constrain external network access. Undeclared outbound requests can expose user data such as ISBNs, reading inventory, or vault-derived metadata to third parties and can bypass user expectations about local-only processing.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The workflow explicitly recommends Google-based enrichment, but the skill description and permissions model do not clearly declare or constrain external network access. Undeclared outbound requests can expose user data such as ISBNs, reading inventory, or vault-derived metadata to third parties and can bypass user expectations about local-only processing.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The workflow explicitly recommends Google-based enrichment, but the skill description and permissions model do not clearly declare or constrain external network access. Undeclared outbound requests can expose user data such as ISBNs, reading inventory, or vault-derived metadata to third parties and can bypass user expectations about local-only processing.

Ae1

High
Category
analysis-evasion
Content
- Goodreads CSV migration with `scripts/migrate_goodreads_csv.py`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
5. Upsert notes with `scripts/upsert_obsidian_note.py`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill requests behavior that implies broad capabilities including environment access, filesystem reads/writes, network use, and shell execution, but it does not declare any explicit tool scope or permissions boundary. This increases the risk of over-privileged execution and makes it harder for reviewers or runtime policy to constrain what the skill may do, especially since it directs execution of local scripts and external enrichment steps.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The workflow instructs the agent to execute shell scripts as part of normal operation, which materially expands the attack surface beyond simple book metadata processing. If those scripts are modified, replaced, or contain unsafe behavior, the agent could perform unintended commands on the local system, access sensitive files, or exfiltrate data under the guise of a routine workflow.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The configuration explicitly enables Google Books enrichment and documents API key use, but it does not clearly warn users that book metadata derived from CSV rows, ISBNs, or OCR/barcode extraction may be transmitted to third-party services. In this skill context, users may process personal library data or photos, so the lack of a clear privacy/data-sharing notice can lead to unintentional disclosure to external providers.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The runbook provides a live migration command that will write or update notes in the user's Obsidian vault, but it does not explicitly warn that this operation modifies existing content. In this skill context, the behavior is expected, but the missing warning increases the chance of accidental destructive or confusing changes to a personal knowledge base, especially because the runbook also mentions idempotent updates and preserving user notes without clearly defining the boundaries of modification.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The runbook instructs querying Google Books for all rows, which causes outbound network requests based on a user's library data, yet it does not disclose this external data flow or any privacy implications. While ISBN/title lookups are core to the skill's purpose, silently sending metadata for every row can surprise users and may expose reading interests or collection details to a third-party service.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
warnings: List[str] = []
    values: List[str] = []
    try:
        run = subprocess.run(
            [binary, "--quiet", image_path],
            check=False,
            capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The script sends title, author, and possibly ISBN data to the Google Books API by default, which discloses user library metadata to a third-party service. In the context of a personal reading-history migration tool, this can reveal sensitive interests or habits without strong in-script disclosure or explicit opt-in at the point of use.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
Outside dry-run mode, the script writes notes via upsert_note, may rename files into shelf folders, and can delete a duplicate source note with unlink(). While these operations fit the migration purpose, this file does not clearly warn users in its docstring, comments, or CLI help that existing vault contents may be modified, moved, or removed.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
},
        timeout_sec=2,
        google_delay_ms=100,
        google_max_retries=0,
        enrich_google=False,
        google_api_key="",
    )
Confidence
80% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The generated summary fallback is always written in Portuguese (`Obra de ... publicada por ...`), which forces a specific language for note content regardless of user preference. The file also uses Portuguese section labels elsewhere, but this line clearly creates user-visible output with no documented language choice or opt-in.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
When `target_note` is provided, the code may overwrite an existing file by writing `new_content` directly if the content differs, with no confirmation prompt or user-facing disclosure at the point of modification. Although the module docstring says it performs an upsert, there is no visible warning near the write path that an existing note may be modified.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
This markdown template uses Portuguese section headings such as "Sinopse" and "Why Read"/other labels without indicating that the skill is intentionally locale-specific or allowing user opt-in to another language. That can violate language/locale policy when a skill implicitly forces a specific language for generated content.

Natural-Language Policy Violations

Low
Confidence
86% confidence
Finding
The configuration sets `BOOK_CAPTURE_OCR_LANG=eng`, which establishes English as the default OCR language in natural-language-facing configuration guidance. Because the document does not mention user opt-in, alternative language selection, or a region-specific justification, this appears to force a locale/language preference.

Natural-Language Policy Violations

Low
Confidence
80% confidence
Finding
The output shape mandates keeping the heading '## Sinopse', which enforces a specific language convention in the generated notes. The file does not document this as an optional locale choice or justify it as a region-specific requirement.

Unverifiable Dependency: pytest has 2 known advisory(ies) (CVE-2025-71176 (pytest has vulnerable tmpdir handling); CVE-2025-71176 (pytest has vulnerable tmpdir handling)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Unverifiable Dependency: requests has 16 known advisory(ies) (CVE-2014-1830 (Exposure of Sensitive Information to an Unauthorized Actor in Requests); CVE-2024-47081 (Requests vulnerable to .netrc credentials leak via malicious URLs); CVE-2024-35195 (Requests `Session` object does not verify requests after making first request wi) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Unverifiable Dependency: Pillow has 16 known advisory(ies) (CVE-2016-2533 (Pillow buffer overflow in ImagingPcdDecode); CVE-2023-50447 (Arbitrary Code Execution in Pillow); CVE-2021-27922 (Pillow Uncontrolled Resource Consumption) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Missing User Warnings

Low
Confidence
88% confidence
Finding
This code performs a file-modifying operation by calling `upsert_note` to create or update a note in the user's vault. While the script purpose suggests ingestion into Obsidian, this file provides no confirmation prompt or explicit user-facing disclosure that running it will write to the vault.

Intent-Code Divergence

Low
Confidence
96% confidence
Finding
The module docstring and script purpose describe deterministic migration behavior, and the self-check is meant to validate that behavior. However, `_build_payload` computes `status` at L274 but never inserts it into `payload` at L276-L293, while `_self_check` asserts `sample_payload.get("status") == "finished"`, so the embedded validation contradicts actual code behavior and will report failure for the wrong reason.

Static analysis

No suspicious patterns detected.