Back to skill

Security audit

Enhanced Memory

Security checks for vulnerabilities and agentic risk

Overview

This memory-search skill is mostly purpose-aligned, but it can copy sensitive memory/workspace notes into an index and send source text or queries to an embedding endpoint that is not constrained to local Ollama.

Install only if you are comfortable with this skill reading memory files and selected workspace-level notes. Keep OLLAMA_URL pointed at a trusted local Ollama instance, treat memory/vectors.json as sensitive because it contains copied plaintext chunks, and avoid running it in shared environments without tightening file permissions and endpoint controls.

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/embed_memories.py:16
Finding
Environment-controlled embedding endpoint can receive sensitive agent memory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/embed_memories.py:16-18, 22, 31-43, 80-84, 89-108`; `scripts/search_memory.py:20-22, 145-149, 226-245` **Vulnerability Type**: Unrestricted transmission of sensitive data to a configurable network endpoint **Risk Level**: High ### Vulnerable Code From `scripts/embed_memories.py`: ```python MEMORY_DIR = os.environ.get('MEMORY_DIR', os.path.join(os.path.dirname(__file__), '..', '..', '..', 'memory')) VECTORS_FILE = os.path.join(MEMORY_DIR, 'vectors.json') OLLAMA_URL = os.environ.get('OLLAMA_URL', 'http://localhost:11434/api/embed') MODEL = os.environ.get('EMBED_MODEL', 'nomic-embed-text') # Core workspace files to also index (relative to workspace root) CORE_FILES = ['MEMORY.md', 'AGENTS.md', 'USER.md', 'SOUL.md', 'research.md'] def get_md_files(memory_dir): """Collect all .md files from memory directory and core workspace files.""" files = [] for root, _, fnames in os.walk(memory_dir): for f in fnames: if f.endswith('.md'): files.append(os.path.join(root, f)) workspace = os.path.abspath(os.path.join(memory_dir, '..')) for name in CORE_FILES: path = os.path.join(workspace, name) if os.path.exists(path): files.append(os.path.abspath(path)) return files ``` ```python def embed_batch(texts): """Get embeddings for a batch of texts from Ollama.""" data = json.dumps({'model': MODEL, 'input': texts}).encode() req = urllib.request.Request(OLLAMA_URL, data=data, headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(req, timeout=60) as resp: return json.loads(resp.read())['embeddings'] ``` ```python def main(): memory_dir = sys.argv[1] if len(sys.argv) > 1 else MEMORY_DIR memory_dir = os.path.abspath(memory_dir) workspace_root = os.path.abspath(os.path.join(memory_dir, '..')) files = get_md_files(memory_dir) all_chunks = [] for f in files: a ...[truncated 4635 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict the embedding endpoint to loopback addresses by default. Parse the configured URL and reject hosts other than `localhost`, `127.0.0.1`, and `::1`. 2. Require a separate, explicit option such as `--allow-remote-embedding` before permitting a non-loopback endpoint. 3. Require HTTPS for every non-loopback endpoint and retain normal certificate validation. 4. Display the destination hostname and the files selected for indexing before transmitting content to a remote service, then require explicit confirmation. 5. Do not automatically index sensitive core files. Replace `CORE_FILES` with an explicit allowlist selected by the user. 6. Provide exclusion controls for private files, directories, and content patterns. 7. Clearly document that embedding requests contain source text rather than only derived vectors. 8. Consider separate configuration variables for trusted local Ollama and remote providers, with remote providers disabled by default. 9. Minimize pseudo-relevance feedback disclosure by performing expansion locally or requiring explicit consent before sending memory-derived terms. 10. Add tests verifying that malformed URLs, non-HTTP schemes, remote HTTP endpoints, credential-bearing URLs, and unexpected hosts are rejected. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/embed_memories.py:100
Finding
Indexed memory content is duplicated in a plaintext file without enforced restrictive permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/embed_memories.py:49-75, 100-108` **Vulnerability Type**: Plaintext storage of sensitive data with ambient file permissions **Risk Level**: Low ### Vulnerable Code ```python def split_into_chunks(filepath, workspace_root): """Split a markdown file into chunks by headers.""" with open(filepath) as f: content = f.read() chunks = [] header = os.path.basename(filepath) lines = [] start = 1 for i, line in enumerate(content.split('\n'), 1): if re.match(r'^#{1,4}\s', line): if lines: text = '\n'.join(lines).strip() if text and len(text) > 20: chunks.append({ 'file': os.path.relpath(filepath, workspace_root), 'header': header, 'line': start, 'text': text, }) header = line.lstrip('#').strip() lines = [line] start = i else: lines.append(line) if lines: text = '\n'.join(lines).strip() if text and len(text) > 20: chunks.append({ 'file': os.path.relpath(filepath, workspace_root), 'header': header, 'line': start, 'text': text, }) return chunks ``` ```python for i in range(0, len(all_chunks), 20): batch = all_chunks[i:i + 20] texts = [c['text'][:2000] for c in batch] vectors = embed_batch(texts) for chunk, vec in zip(batch, vectors): chunk['embedding'] = vec) done = min(i + 20, len(all_chunks)) print(f' {done}/{len(all_chunks)} chunks embedded') vectors_path = os.path.join(memory_dir, 'vectors.json') with open(vectors_path, 'w') as f: json.dump(all_chunks, f, indent=None) print(f'Saved {len(all_chunks)} vectors to {vectors_path}') ``` ### ...[truncated 2332 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the index with owner-only permissions rather than relying on the ambient umask. 2. Write to a temporary file in the same protected directory using mode `0600`, flush and synchronize it, then atomically replace the destination with `os.replace`. 3. If the destination already exists, verify and correct its permissions with `os.chmod(vectors_path, 0o600)`. 4. Ensure the memory directory itself is not accessible to unauthorized local users, preferably with mode `0700`. 5. Avoid retaining full plaintext chunks unless search functionality requires them. Consider storing minimal metadata and loading result text from the original protected file when needed. 6. If plaintext retention is necessary, document that `vectors.json` is sensitive and must receive the same protection as the original memory files. 7. Consider encryption at rest where the threat model includes other privileged local processes or untrusted storage. 8. Add automated tests that run under permissive umask settings and verify that the final index remains owner-readable and owner-writable only. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (13)

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

Critical
Category
Data Flow
Content
"""Get embeddings for a batch of texts from Ollama."""
    data = json.dumps({'model': MODEL, 'input': texts}).encode()
    req = urllib.request.Request(OLLAMA_URL, data=data, headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=60) as resp:
        return json.loads(resp.read())['embeddings']
Confidence
97% confidence
Finding
The script sends chunked contents of local markdown files to a network endpoint taken directly from the OLLAMA_URL environment variable, with no validation that it is local or trusted. Because the indexed content includes memory files and selected workspace files, a user or wrapper can redirect embeddings to an arbitrary remote server and exfiltrate potentially sensitive project data.

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

Critical
Category
Data Flow
Content
"""Get embedding vector from Ollama."""
    data = json.dumps({'model': MODEL, 'input': [text]}).encode()
    req = urllib.request.Request(OLLAMA_URL, data=data, headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())['embeddings'][0]
Confidence
96% confidence
Finding
The request target is derived from the OLLAMA_URL environment variable and user query text is sent to that endpoint without validation or restriction. This enables SSRF-style behavior or silent exfiltration of sensitive prompts/queries if an attacker can influence the environment, and the risk is amplified because this skill processes memory-search content that may contain sensitive internal data.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description describes a broad, enhanced memory retrieval/search subsystem with multiple scoring signals and search-time behaviors. The supplied code chunk is much narrower: it builds semantic cross-references using existing embeddings and cosine similarity, plus simple reporting commands. The only notable overlap is the 'knowledge graph cross-references' aspect and the declared use case of building cross-references. However, the primary declared purpose—an advanced 4-signal memory search replacement—is not represented in this code. Therefore the description materially overstates and mischaracterizes what this specific code chunk actually does.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The declared description presents a full 4-signal memory search/retrieval and cross-reference system. The supplied code chunk only performs offline embedding/index construction: collecting markdown files, chunking by headers, calling Ollama's embed API, and saving vectors. While indexing memory files is one small part of the declaration, the primary claimed behavior is a sophisticated search/scoring system that is absent here. Additionally, the script indexes not only the memory directory but also workspace-level files such as MEMORY.md, AGENTS.md, USER.md, SOUL.md, and research.md, which is broader than a plain memory-file embedding description. Therefore the description materially overstates and misrepresents what this code chunk actually does.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The description presents a broad enhanced memory search/indexing system with multiple advanced retrieval features and an Ollama embedding dependency. The supplied code chunk only scores memory item salience based on file metadata, modification age, access frequency, and simple query-word overlap from logs, then generates reminder prompts. While 'salience scoring' is mentioned in the description, the code does not implement the stated primary search/retrieval capabilities, indexing pipeline, or knowledge graph behavior. This is a material description-behavior mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding
The code substantially matches the core search-related portion of the description: it performs 4-signal fusion retrieval (vector, keyword, header, filepath), temporal routing via date extraction and file-path date boosts, adaptive weighting, and pseudo-relevance feedback, and it uses Ollama with the nomic-embed-text model by default. However, the declared purpose materially overstates the implemented scope by claiming use for indexing memory files, building cross-references, and scoring memory salience. This code only searches an existing vectors.json index and does not create/update indexes, compute salience, or build/use a knowledge graph. Because those are presented as core capabilities/use cases rather than minor extras, this is a description-behavior mismatch.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
prompts.append(f"📝 Daily notes from {item['name']} haven't been reviewed for MEMORY.md.")
        elif item['type'] == 'core' and item['age_days'] > 2:
            prompts.append(f"🧠 MEMORY.md last updated {item['age_days']} days ago.")
    return prompts


def main():
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill advertises and instructs use of scripts that read and write workspace files and make local network requests to Ollama, but the manifest declares no explicit tool scope or permissions. Missing scope boundaries can cause an agent platform to over-grant capabilities or leave reviewers unable to assess whether file and network access are intended, increasing the chance of unintended data access or exfiltration through the referenced scripts.

Tainted flow: 'CROSSREF_FILE' from os.environ.get (line 22, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
'file_graph': file_graph,
    }

    with open(CROSSREF_FILE, 'w') as f:
        json.dump(result, f, indent=2)

    print(f'\nCross-reference index built:')
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The environment-configurable OLLAMA_URL allows all indexed content to be sent to any HTTP endpoint, not just a local Ollama instance. Combined with the skill's purpose of sweeping memory and selected workspace files, this creates a straightforward exfiltration channel for sensitive local data under the guise of embedding generation.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The script does not limit indexing to the memory directory; it also pulls in workspace-level files like MEMORY.md, AGENTS.md, USER.md, SOUL.md, and research.md. In this skill context, those files may contain broader instructions, personal notes, or operational data that users would not expect to be ingested and embedded when running a memory indexing tool.

Tainted flow: 'vectors_path' from os.environ.get (line 115, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
print(f'  {done}/{len(all_chunks)} chunks embedded')

    vectors_path = os.path.join(memory_dir, 'vectors.json')
    with open(vectors_path, 'w') as f:
        json.dump(all_chunks, f, indent=None)

    print(f'Saved {len(all_chunks)} vectors to {vectors_path}')
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The code transmits the user's search query to an embedding service over HTTP without any explicit notice, consent flow, or local-only enforcement in this file. In a memory-search skill, queries often include sensitive personal, organizational, or secret-bearing context, so silent transmission creates a meaningful privacy and data-handling risk even if the service is intended to be local.

Static analysis

No suspicious patterns detected.