Back to skill

Security audit

The Librarian

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it claims, but it has review-worthy risks around unsafe index loading and under-disclosed transmission of document text to an embedding service.

Install only if you are comfortable reviewing and controlling the index directory, embedding endpoint, and dependencies. Do not search indexes received from others unless bm25_index.pkl is removed or independently trusted. Use a local trusted Ollama endpoint, avoid remote HTTP APIs for sensitive documents, and protect generated index directories because chunks.json stores document text.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/build_index.py:52
Finding

Document Contents Can Be Transmitted to an Arbitrary Plaintext Embedding Endpoint

Content
View full analysis
list[float]: """Get embedding from Ollama API.""" response = requests.post( f"{api_url}/api/embeddings", json={"model": model, "prompt": text}, timeout=120 ) response.raise_for_status() return response.json()["embedding"] ``` The document chunks are passed directly to this function: ```python emb = get_embedding(chunk["text"], model, api_url) ``` The destination is caller-controlled: ```python parser.add_argument( "--api", "-a", default=DEFAULT_OLLAMA_API, help=f"Ollama API URL (default: {DEFAULT_OLLAMA_API})" ) ``` ### Technical Analysis Every indexed document is divided into chunks and placed in the `prompt` property of an HTTP POST request. The `--api` option permits the caller to select an arbitrary destination without: - Restricting the destination to loopback or another approved embedding service. - Requiring HTTPS for non-local endpoints. - Warning the user that document contents will leave the process. - Validating the hostname, IP address, port, URL scheme, or redirects. - Requiring explicit authorization before remote document processing. Use of an embedding service is necessary for semantic indexing. However, allowing unrestricted destinations exceeds the minimum network privileges needed for a locally oriented skill. It also conflicts with the offline and “No cloud” positioning in `SKILL.md`. This behavior creates both a confidentiality risk and an SSRF-like network-access primitive. The process can be instructed to send attacker-selected document content to an external server or make requests to internal HT ...[truncated 1568 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/search.py:58
Finding

Search Queries Can Be Transmitted to an Arbitrary Plaintext Embedding Endpoint

Content
View full analysis
list[float]: """Get embedding from Ollama API.""" response = requests.post( f"{api_url}/api/embeddings", json={"model": model, "prompt": text}, timeout=60 ) response.raise_for_status() return response.json()["embedding"] ``` The query is supplied to the embedding request: ```python query_emb = get_embedding(query, model, api_url) ``` The destination is caller-controlled: ```python parser.add_argument( "--api", "-a", default=DEFAULT_OLLAMA_API, help=f"Ollama API URL (default: {DEFAULT_OLLAMA_API})" ) ``` ### Technical Analysis Vector and hybrid searches send the complete search query to an embedding endpoint. The endpoint is controlled through `--api`, with no hostname allowlist, TLS enforcement, redirect policy, or explicit warning about remote query processing. Search queries can reveal sensitive subjects, client names, incident details, internal project identifiers, or the type of information stored in a private index. The use of ordinary HTTP also provides no transport confidentiality or server authentication when the destination is not a genuinely local connection. The network request is required for the current vector-search implementation, but unrestricted remote destination selection is broader than required for local Ollama integration. ### Attack Path 1. The victim has a local index containing private material. 2. An attacker causes the victim or an automation layer to use an attacker-controlled API destination: ```bash python scripts/search.py \ "confidential acquisition target" \ --index ./private- ...[truncated 988 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/search.py:96
Finding

Unsafe Pickle Deserialization Allows Code Execution When Loading an Index

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:43
Finding

Unpinned Third-Party Dependencies Create a Mutable Supply-Chain Risk

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • YARA SignaturesMalware Match, Webshell Match, Cryptominer Match
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (15)

YARA rule 'backdoor_persistence': Backdoor persistence with malicious payloads (shell commands, SSH key injection, hidden root users) [malware]

High
Category
YARA Match
Confidence
75% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · scripts/build_index.py (reported line 30)May include surrounding context.

python
E = 'turbovec'
except ImportError:
    print("ERROR: TurboVec not available. Install with: pip install turbovec")
    print("BLAS library required. Install with: sudo apt install libblas3")
    print("")
    print("Run via the wrapper script (recommended):")
    print("  ./scripts/librarian.sh build <input> <output>")
    print("")
    print("Or set LD_PRELOAD before Python starts:")
    print("  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libblas.so.3 python build_index.py ...")
    sys.exit(1)

import numpy as np
import requests

# Optional: BM25 for hybrid search
try:
    from rank_bm25 import BM25Okapi
    BM25_AVAILABLE = True
except ImportError:
    BM25_AVAILABLE = False
    print("Note: BM25 not available. Install with: pip install rank-bm25")

# Configuration
DEFAULT_OLLAMA_API = "http://host.docker.internal:11434"
DEFAULT_MODEL = "nomic-embed-text:v1.5"
CHUNK_SIZE = 650  # Characters per chunk
CHUNK_OVERLAP = 100  # Characters overlap between chunks
BIT_WIDTH = 4  # 2, 3, or 4 bits

YARA rule 'backdoor_persistence': Backdoor persistence with malicious payloads (shell commands, SSH key injection, hidden root users) [malware]

High
Category
YARA Match
Confidence
75% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · SKILL.md (reported line 61)May include surrounding context.

md
E = 'turbovec'
except ImportError:
    print("ERROR: TurboVec not available. Install with: pip install turbovec")
    print("BLAS library required. Install with: sudo apt install libblas3")
    print("")
    print("Run via the wrapper script (recommended):")
    print("  ./scripts/librarian.sh search <query> <index>")
    print("")
    print("Or set LD_PRELOAD before Python starts:")
    print("  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libblas.so.3 python search.py ...")
    sys.exit(1)

import numpy as np
import requests

# Optional: BM25 for hybrid search
try:
    from rank_bm25 import BM25Okapi
    BM25_AVAILABLE = True
except ImportError:
    BM25_AVAILABLE = False

# Optional: Flashrank for reranking
try:
    from flashrank import Ranker, RerankRequest
    FLASHRANK_AVAILABLE = True
except ImportError:
    FLASHRANK_AVAILABLE = False

# Configuration
DEFAULT_OLLAMA_API = "http://host.docker.internal:11434"
DEFAULT_MODEL = "nomic-embed-text:v1.5"


def get_embedding(text: str, model:

YARA rule 'backdoor_persistence': Backdoor persistence with malicious payloads (shell commands, SSH key injection, hidden root users) [malware]

High
Category
YARA Match
Confidence
75% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · scripts/search.py (reported line 33)May include surrounding context.

python
E = 'turbovec'
except ImportError:
    print("ERROR: TurboVec not available. Install with: pip install turbovec")
    print("BLAS library required. Install with: sudo apt install libblas3")
    print("")
    print("Run via the wrapper script (recommended):")
    print("  ./scripts/librarian.sh search <query> <index>")
    print("")
    print("Or set LD_PRELOAD before Python starts:")
    print("  LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libblas.so.3 python search.py ...")
    sys.exit(1)

import numpy as np
import requests

# Optional: BM25 for hybrid search
try:
    from rank_bm25 import BM25Okapi
    BM25_AVAILABLE = True
except ImportError:
    BM25_AVAILABLE = False

# Optional: Flashrank for reranking
try:
    from flashrank import Ranker, RerankRequest
    FLASHRANK_AVAILABLE = True
except ImportError:
    FLASHRANK_AVAILABLE = False

# Configuration
DEFAULT_OLLAMA_API = "http://host.docker.internal:11434"
DEFAULT_MODEL = "nomic-embed-text:v1.5"


def get_embedding(text: str, model:

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

This finding correctly identifies unsafe deserialization in search-path code that does not require executable object restoration to fulfill its document-search purpose. In this context, loading a crafted BM25 pickle from an index directory can result in arbitrary code execution before any meaningful search validation occurs.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill documents commands that read local documents, write indexes/chunks to disk, and install packages from the network, but it does not declare any explicit tool scope or permission boundaries. This can mislead users or host platforms about the skill's operational reach, increasing the chance of unintended access to sensitive files or unreviewed network activity.

Content

No source excerpt is available for this finding.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 42)May include surrounding context.

bash
# Install BLAS library (required for TurboVec)
sudo apt install libblas3

# Create venv and install dependencies
cd /path/to/the-librarian

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · scripts/build_index.py (reported line 24)May include surrounding context.

python
ENGINE = 'turbovec'
except ImportError:
    print("ERROR: TurboVec not available. Install with: pip install turbovec")
    print("BLAS library required. Install with: sudo apt install libblas3")
    print("")
    print("Run via the wrapper script (recommended):")
    print("  ./scripts/librarian.sh build <input> <output>")

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · scripts/librarian.sh (reported line 26)May include surrounding context.

sh
ENGINE = 'turbovec'
except ImportError:
    print("ERROR: TurboVec not available. Install with: pip install turbovec")
    print("BLAS library required. Install with: sudo apt install libblas3")
    print("")
    print("Run via the wrapper script (recommended):")
    print("  ./scripts/librarian.sh build <input> <output>")

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · scripts/search.py (reported line 27)May include surrounding context.

python
ENGINE = 'turbovec'
except ImportError:
    print("ERROR: TurboVec not available. Install with: pip install turbovec")
    print("BLAS library required. Install with: sudo apt install libblas3")
    print("")
    print("Run via the wrapper script (recommended):")
    print("  ./scripts/librarian.sh build <input> <output>")

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The script sends raw document chunk text to an embedding API via HTTP without clearly warning the user that document contents will be transmitted off-process and potentially off-host. In this skill's context, the indexed data may contain sensitive documents, so silent transmission to a configurable endpoint creates a real confidentiality risk.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
98% confidence
Finding

The requests.post call transmits document contents in the prompt field to the embedding service specified by api_url. Because api_url is user-configurable and defaults to host.docker.internal, this can expose sensitive local documents to another service or machine without transport guarantees or trust validation.

Content

Scanner excerpt · scripts/build_index.py (reported line 54)May include surrounding context.

python
def get_embedding(text: str, model: str, api_url: str) -> list[float]:
    """Get embedding from Ollama API."""
    response = requests.post(
        f"{api_url}/api/embeddings",
        json={"model": model, "prompt": text},
        timeout=120

External Transmission

Medium
Category
Data Exfiltration
Confidence
81% confidence
Finding

The script performs an external network request containing raw query text to the configured embedding endpoint. Even if intended for local Ollama use, the endpoint is configurable, and use of plain HTTP means sensitive queries could be exposed to a remote service or intercepted on the network.

Content

Scanner excerpt · scripts/search.py (reported line 60)May include surrounding context.

python
def get_embedding(text: str, model: str, api_url: str) -> list[float]:
    """Get embedding from Ollama API."""
    response = requests.post(
        f"{api_url}/api/embeddings",
        json={"model": model, "prompt": text},
        timeout=60

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The user's query text is sent over HTTP to an embedding API without a clear privacy warning or consent step. Search queries may contain sensitive document topics, secrets, names, or internal project information, so silent transmission can create confidentiality and compliance risks, especially when the endpoint is changed from the default local service.

Content

No source excerpt is available for this finding.

Insecure deserialization: pickle.load()

Medium
Category
Dangerous Code Execution
Confidence
98% confidence
Finding

The code deserializes bm25_index.pkl using pickle.load, which can execute arbitrary code during loading if the file is attacker-controlled or tampered with. Because the path is user-supplied via --index, an operator can be induced to open an untrusted index directory, turning a search operation into local code execution.

Content

Scanner excerpt · scripts/search.py (reported line 100)May include surrounding context.

python
if bm25_file.exists():
        try:
            with open(bm25_file, 'rb') as f:
                bm25 = pickle.load(f)
        except Exception as e:
            print(f"Warning: Could not load BM25 index: {e}")

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

The skill instructs users to build indexes that persist document chunks, metadata, and optional BM25 artifacts locally, but it does not clearly warn that potentially sensitive source content will be copied into output files. In a document-search skill, this omission is contextually important because users may index confidential material without realizing how much content is retained.

Content

No source excerpt is available for this finding.