Back to skill

Security audit

OEE Knowledge Base RAG

Security checks for vulnerabilities and agentic risk

Overview

This is a plausible personal knowledge-base skill, but it stores and sends saved content externally and contains unsafe URL/PDF handling that should be reviewed before use.

Review before installing. Do not use this skill with secrets, regulated data, private company documents, internal URLs, or untrusted PDFs unless it is remediated. It should clearly disclose remote AI-provider processing, add confirmation/local-only controls, restrict URL destinations, and fix the PDF and temporary-file handling issues.

Vulnerability Patterns
  • 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
  • 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
kb.py:204
Finding
Arbitrary Code Execution Through Python Source Injection in PDF Extraction<![CDATA[ ## Vulnerability Details **File Location**: `kb.py:204-217` **Vulnerability Type**: Python source injection through an unescaped file path **Risk Level**: High ### Vulnerable Code ```python result = subprocess.run( ["python3", "-c", f""" import sys try: import fitz doc = fitz.open("{path}") for page in doc: print(page.get_text()) except ImportError: import subprocess r = subprocess.run(["pdftotext", "{path}", "-"], capture_output=True, text=True) print(r.stdout) """], capture_output=True, text=True, timeout=60 ) ``` ### Technical Analysis The `path` value ultimately originates from the positional command-line argument accepted by `ingest.py`. When the argument is classified as a PDF, `extract_pdf()` interpolates it directly into source code passed to `python3 -c`. Although `subprocess.run()` uses an argument array and does not invoke a shell, this does not prevent injection into the dynamically generated Python program. A local filename containing quotation marks, line breaks, or Python syntax can terminate the quoted argument to `fitz.open()` and introduce attacker-controlled statements. The same unescaped value appears a second time in the fallback `pdftotext` call. Escaping only shell metacharacters would therefore be insufficient; the fundamental issue is generation of executable source code from untrusted input. ### Attack Path 1. An attacker creates or supplies a local filename containing embedded Python syntax while ensuring the final name ends in `.pdf`. 2. The victim or an agent invokes: ```bash python ingest.py "/path/to/crafted-name.pdf" ``` 3. `classify_url()` classifies the supplied value as a PDF. 4. `extract_pdf()` inserts the path into the formatted `python3 -c` program. 5. The crafted path terminates the intended Python string and adds arbitrary Python statements. 6. The subprocess executes those statements with the privileges and environment of the user running the Skill. ### Impact As ...[truncated 460 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not construct Python source code using the PDF path. - Prefer importing and invoking PyMuPDF directly in the current process: ```python import fitz with fitz.open(path) as doc: text = "\n".join(page.get_text() for page in doc) ``` - If isolation in a child process is required, use a fixed program and pass the path through `sys.argv`: ```python subprocess.run( ["python3", "-c", FIXED_SCRIPT, str(path)], capture_output=True, text=True, timeout=60, check=True, ) ``` - Invoke `pdftotext` directly with an argument array rather than embedding its invocation in generated Python. - Resolve and validate local paths before use, reject unexpected file types, and verify that the target is a regular file rather than a symbolic link. - Add regression tests using filenames containing quotation marks, backslashes, newlines, and Python metacharacters. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
kb.py:104
Finding
Server-Side Request Forgery Through Unrestricted User-Supplied URLs<![CDATA[ ## Vulnerability Details **File Locations**: `kb.py:104-109`, `kb.py:193-199`, `kb.py:229-231` **Vulnerability Type**: Unrestricted outbound request and SSRF **Risk Level**: High ### Vulnerable Code ```python def _api_get(url: str, timeout: int = 15) -> str: req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (compatible; OEEBot/1.0)"}) with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode("utf-8", errors="replace") ``` ```python def extract_pdf(url_or_path: str) -> tuple[str, str]: """Extract text from PDF.""" import tempfile path = url_or_path if url_or_path.startswith("http"): tmp = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) data = urllib.request.urlopen(url_or_path, timeout=30).read() tmp.write(data) tmp.close() path = tmp.name ``` ```python def extract_article(url: str) -> tuple[str, str]: """Extract article content with readability → raw fallback.""" raw = _retry(lambda: _api_get(url)) ``` ### Technical Analysis The URL supplied through `ingest.py` is passed to `urllib.request.urlopen()` without enforcing an allowed scheme or destination policy. The implementation does not reject loopback, private, link-local, multicast, reserved, or cloud metadata addresses. It also does not revalidate destinations after redirects. Consequently, the Skill can be used as a request proxy from the host on which it runs. Its network position may allow it to reach endpoints unavailable to an external attacker, including localhost services, private network APIs, container control interfaces, and cloud instance metadata services. Fetched content is not merely returned to the caller. It can subsequently be stored in SQLite and sent to the configured embedding provider, which can turn SSRF into a data-exfiltration path. ### Attack Path 1. An attacker persuades an agent or user to ingest a URL targeting an internal resource, su ...[truncated 1136 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Allow only explicitly supported `https` and, if necessary, `http` URLs. Reject all other schemes. - Resolve the hostname before connecting and reject every loopback, private, link-local, multicast, unspecified, and reserved IP address for both IPv4 and IPv6. - Re-resolve and revalidate the destination after every redirect. - Defend against DNS rebinding by connecting only to the validated address while preserving correct TLS hostname verification. - Consider an explicit domain allowlist when the Skill is used in a sensitive environment. - Block known cloud metadata destinations and ensure outbound firewall rules independently deny metadata and internal management networks. - Apply strict response-size and content-type limits before buffering responses. - Separate public URL ingestion from local file ingestion so that strings are not ambiguously treated as either paths or URLs. - Do not automatically forward fetched content to an external embedding service without an explicit privacy decision. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
kb.py:302
Finding
Sensitive Knowledge-Base Content and Queries Are Transmitted to External AI Providers<![CDATA[ ## Vulnerability Details **File Locations**: `kb.py:302-319`, `kb.py:408-410`, `kb.py:448-485`, `kb.py:508-510`; disclosure issue in `SKILL.md:46-48` **Vulnerability Type**: External disclosure of potentially sensitive user content **Risk Level**: Medium ### Vulnerable Code ```python def _openai_embed(texts: list[str]) -> list[list[float]]: """Call OpenAI embeddings API.""" api_key = os.environ.get("OPENAI_API_KEY") if not api_key: raise RuntimeError("OPENAI_API_KEY not set") payload = json.dumps({"input": texts, "model": EMBED_MODEL}).encode() req = urllib.request.Request( "https://api.openai.com/v1/embeddings", data=payload, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, ) for attempt in range(EMBED_RETRIES): try: with urllib.request.urlopen(req, timeout=30) as r: data = json.loads(r.read()) ``` ```python def retrieve(query: str, top_k: int = 10) -> list[dict]: """Find most relevant chunks for a query.""" db = get_db() q_emb = embed_query(query) ``` ```python context = "\n\n---\n\n".join(context_parts) api_key = os.environ.get("ANTHROPIC_API_KEY") if not api_key: raise RuntimeError("ANTHROPIC_API_KEY not set") system = "You are a helpful research assistant. Answer using ONLY the provided context. Cite sources using [n] notation. If the context doesn't contain enough information, say so." user_msg = f"Context:\n\n{context}\n\n---\n\nQuestion: {query}" payload = json.dumps({ "model": "claude-sonnet-4-20250514", "max_tokens": 2048, "system": system, "messages": [{"role": "user", "content": user_msg}], }).encode() req = urllib.request.Request( "https://api.anthropic.com/v1/messages", data=payload, headers={ "x-api-key": api_key, "anthropic-version": "2023-06-01", "Content-Type": "application/json", }, ) ``` The documentation states only: ``` ...[truncated 2445 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Clearly disclose before installation and use that: - ingested plaintext is sent to OpenAI; - search queries are sent to OpenAI; - generated-answer mode sends selected knowledge-base content and queries to Anthropic. - Correct `SKILL.md` to state the actual key requirements and distinguish embeddings from answer generation. - Obtain explicit user consent before the first external transmission. - Add local embedding and local language-model options so users can operate entirely offline. - Provide a strict no-network mode and fail closed when it is enabled. - Allow users to mark sources as local-only or sensitive so they are excluded from external processing. - Apply configurable redaction for credentials, tokens, personal data, and other sensitive patterns before transmission. - Minimize transmitted context and document provider retention, privacy, and data-processing terms. - Consider storing only embeddings and encrypted source content where local confidentiality requirements justify it. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
kb.py:153
Finding
Predictable Shared Temporary Files Permit Transcript Poisoning and Local File Disclosure<![CDATA[ ## Vulnerability Details **File Location**: `kb.py:153-187` **Vulnerability Type**: Unsafe predictable temporary-file handling and symlink race **Risk Level**: Medium ### Vulnerable Code ```python result = subprocess.run( ["yt-dlp", "--write-auto-sub", "--sub-lang", "en", "--skip-download", "--sub-format", "vtt", "-o", "/tmp/kb_yt_%(id)s", url], capture_output=True, text=True, timeout=60 ) vid_id = re.search(r'[?&]v=([^&]+)', url) or re.search(r'youtu\.be/([^?]+)', url) vid_id = vid_id.group(1) if vid_id else "" vtt_path = None for ext in [".en.vtt", ".vtt"]: p = Path(f"/tmp/kb_yt_{vid_id}{ext}") if p.exists(): vtt_path = p break if not vtt_path: import glob matches = glob.glob(f"/tmp/kb_yt_{vid_id}*.vtt") if matches: vtt_path = Path(matches[0]) if not vtt_path: raise RuntimeError(f"No transcript found for {url}. yt-dlp stderr: {result.stderr[:500]}") raw = vtt_path.read_text() ... vtt_path.unlink(missing_ok=True) ``` ### Technical Analysis YouTube subtitle output is written beneath the globally shared `/tmp` directory using a predictable filename derived from a public video identifier. The code then searches for matching files and accepts the first glob result without confirming that it was created by the current `yt-dlp` invocation. `Path.exists()` and `Path.read_text()` follow symbolic links. A local attacker can therefore pre-create a matching path or symbolic link before ingestion. The fallback glob broadens the accepted filename set further. The code does not verify file ownership, file type, link status, directory containment, or creation time. A pre-positioned file can poison the knowledge base with attacker-selected text. Where the victim can read the symlink target, it may also cause a local file to be read and subsequently transmitted to OpenAI as transcript content. Cleanup then attempts to unlink the selected path. ### Attack Path 1. A local attacker predicts the vid ...[truncated 1177 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create a private per-operation directory using `tempfile.TemporaryDirectory()`. - Configure `yt-dlp` to write all output inside that private directory rather than directly under `/tmp`. - Set restrictive permissions and ensure the temporary directory is owned by the invoking user. - Enumerate only files inside that directory; do not use a shared-directory glob. - Open candidate files with protections against symbolic-link traversal, such as `O_NOFOLLOW` where supported. - Verify that the selected path is a regular file, is owned by the current user, and remains within the resolved temporary directory. - Use context-managed cleanup of the complete private directory. - Treat failure to produce a fresh subtitle file as an error rather than accepting a pre-existing match. ]]>
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 (20)

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

Critical
Category
Data Flow
Content
def _api_get(url: str, timeout: int = 15) -> str:
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (compatible; OEEBot/1.0)"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return r.read().decode("utf-8", errors="replace")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
for attempt in range(EMBED_RETRIES):
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                data = json.loads(r.read())
            return [d["embedding"] for d in sorted(data["data"], key=lambda x: x["index"])]
        except Exception as e:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
},
    )

    with urllib.request.urlopen(req, timeout=60) as r:
        data = json.loads(r.read())

    answer = data["content"][0]["text"]
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Hidden Instructions

High
Category
Prompt Injection
Content
Save context: `python ingest.py "TEXT"`
Retrieve context: `python query.py "QUESTION"`

<!-- 🐾 Muninn never forgets -->
Confidence
70% confidence
Finding
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill encourages saving arbitrary text, URLs, files, and notes, and it requires external embedding providers via API keys, but it does not warn users that sensitive content may be stored locally in SQLite and transmitted to third-party embedding services. This omission can cause users or agents to send secrets, personal data, or confidential material into storage and external processing without informed consent.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger phrases include generic language such as "remember this" and "what do I know about," which can easily overlap with normal conversation and cause unintended activation. In an agent setting, that can lead to accidental ingestion of user content into persistent storage or retrieval from the knowledge base without sufficiently explicit user intent.

External Transmission

Medium
Category
Data Exfiltration
Content
if not match:
        raise ValueError(f"Not a valid tweet URL: {url}")
    user, tweet_id = match.groups()
    api_url = f"https://api.fxtwitter.com/{user}/status/{tweet_id}"
    try:
        data = json.loads(_retry(lambda: _api_get(api_url)))
        tweet = data.get("tweet", {})
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
if not match:
        raise ValueError(f"Not a valid tweet URL: {url}")
    user, tweet_id = match.groups()
    api_url = f"https://api.fxtwitter.com/{user}/status/{tweet_id}"
    try:
        data = json.loads(_retry(lambda: _api_get(api_url)))
        tweet = data.get("tweet", {})
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Context-Inappropriate Capability

Medium
Confidence
81% confidence
Finding
The module is described as a personal knowledge base with semantic retrieval, but this implementation adds subprocess execution via yt-dlp for YouTube transcript extraction. Without any manifest declaring shell/tool execution as part of scope, spawning external binaries is a broader capability than the stated high-level KB purpose implies.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def extract_youtube(url: str) -> tuple[str, str]:
    """Extract YouTube transcript via yt-dlp."""  # 🐾
    try:
        title_out = subprocess.run(
            ["yt-dlp", "--get-title", url],
            capture_output=True, text=True, timeout=30
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
except Exception:
        title = "YouTube Video"

    result = subprocess.run(
        ["yt-dlp", "--write-auto-sub", "--sub-lang", "en", "--skip-download",
         "--sub-format", "vtt", "-o", "/tmp/kb_yt_%(id)s", url],
        capture_output=True, text=True, timeout=60
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
Invoking external tooling to process PDFs materially increases risk because PDFs are attacker-controlled inputs and parser chains are historically bug-prone. In this implementation, the danger is compounded by dynamic code construction for python3 -c, turning a content-extraction feature into a potential local code-execution path when given a crafted local filename.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
path = tmp.name

    try:
        result = subprocess.run(
            ["python3", "-c", f"""
import sys
try:
Confidence
96% confidence
Finding
This code builds Python source with an f-string and injects the PDF path directly into a python3 -c script. If path contains quotes or Python syntax, an attacker controlling the local path argument to extract_pdf can break out of the string literal and execute arbitrary Python code in the subprocess.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The skill sends ingested content to external embedding and model providers using environment-backed credentials without any visible access control, redaction, or sensitivity checks. In a personal knowledge-base context, that can expose stored private notes, fetched documents, or other sensitive material to third parties unexpectedly.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
Text passed to embed_texts is uploaded to OpenAI without any user-facing warning at the transmission point. For a knowledge base that may hold personal or confidential content, silent third-party transmission creates a meaningful privacy and compliance risk.

External Transmission

Medium
Category
Data Exfiltration
Content
payload = json.dumps({"input": texts, "model": EMBED_MODEL}).encode()
    req = urllib.request.Request(
        "https://api.openai.com/v1/embeddings",
        data=payload,
        headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
    )
Confidence
87% confidence
Finding
This line sends potentially sensitive ingested text to OpenAI's embeddings API. In the context of a personal knowledge base, external transmission of document content is a real privacy/security concern if done without clear consent, minimization, or redaction.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
Remote content fetched from arbitrary URLs is embedded via a third-party API without a clear disclosure step, which may surprise users and may violate expectations around handling copyrighted, confidential, or regulated content. The ingestion workflow makes this more concerning because transmission is automatic after fetch and validation.

Ssd 3

Medium
Confidence
93% confidence
Finding
Retrieved chunk content is inserted verbatim into the Anthropic prompt, so any sensitive text previously ingested can be disclosed in plain language to the external model and then to whoever can query the system. In a personal knowledge-base setting, this is especially risky because the corpus may contain secrets, private notes, credentials, or proprietary documents.

External Transmission

Medium
Category
Data Exfiltration
Content
}).encode()

    req = urllib.request.Request(
        "https://api.anthropic.com/v1/messages",
        data=payload,
        headers={
            "x-api-key": api_key,
Confidence
89% confidence
Finding
The ask() flow transmits retrieved KB context verbatim to Anthropic, potentially including sensitive information from previously ingested sources. Because retrieval is query-driven and the content is inserted wholesale, this can expose private corpus contents to an external LLM provider and to unauthorized users of the skill.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
The yt-dlp invocation forces `--sub-lang en`, which imposes a specific language/locale behavior without user opt-in. This can violate language-choice policy because users are not given any option to select another subtitle language or accept the English-only constraint.

Static analysis

No suspicious patterns detected.