Back to skill

Security audit

Zvec Local RAG Service

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local RAG service, but it needs careful review because it exposes unauthenticated local HTTP endpoints that can index readable files and it tries to support persistent background execution.

Install only if you are comfortable running a local background service that can read and index any .txt or .md files reachable by the service account when requested. Keep it bound to 127.0.0.1, do not enable non-loopback hosting unless you add authentication and network controls, avoid pointing ingestion at broad or sensitive directories, and review or pin dependencies before bootstrap. The missing launchd template should be fixed before relying on the persistence workflow.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (3)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/rag-service.mjs:181
Finding
Unauthenticated Arbitrary-Directory Document Ingestion<![CDATA[ ## Vulnerability Details **File Location**: `scripts/rag-service.mjs:64-76`, `scripts/rag-service.mjs:122-128`, and `scripts/rag-service.mjs:181-187` **Vulnerability Type**: Missing authentication and unrestricted filesystem path access **Risk Level**: High when non-loopback binding is enabled; Medium with the default loopback binding ### Vulnerable Code ```js function listTextFiles(dir) { if (!fs.existsSync(dir)) return []; const out = []; const stack = [path.resolve(dir)]; while (stack.length) { const cur = stack.pop(); for (const ent of fs.readdirSync(cur, { withFileTypes: true })) { const p = path.join(cur, ent.name); if (ent.isDirectory()) stack.push(p); else if (/\.(txt|md)$/i.test(ent.name)) out.push(p); } } return out; } ``` ```js async function ingestFromDir(dir, reset = true) { const files = listTextFiles(dir); if (!files.length) return { files: 0, chunks: 0 }; const rows = []; for (const f of files) { const text = fs.readFileSync(f, 'utf8'); ``` ```js if (req.method === 'POST' && req.url === '/ingest') { const body = await readJsonBody(req); const dir = body.dir || './docs'; const reset = body.reset !== false; const out = await ingestFromDir(dir, reset); return json(res, 200, { ok: true, ...out, model: MODEL, dbPath: DB_PATH }); } ``` ### Technical Analysis The `/ingest` endpoint accepts a caller-controlled directory without authentication, authorization, or validation against an approved ingestion root. `path.resolve(dir)` permits absolute paths and traversal expressions to resolve anywhere on the local filesystem. The service then recursively enumerates the selected directory and reads every `.txt` or `.md` file accessible to the service process. The documents are embedded and stored with their original text and source path. The unauthenticated `/search` endpoint can subsequently return indexed text. The default loopback-only listener reduces network exposure, but any ...[truncated 2051 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require authentication and authorization for `/ingest` and `/search`, especially before allowing non-loopback binding. 2. Define one or more explicit ingestion roots in trusted server-side configuration. Do not let clients select arbitrary filesystem locations. 3. Canonicalize both the approved root and requested path using `fs.realpathSync()` and reject requests whose canonical path is not the approved root or one of its descendants. 4. Reject symbolic links or validate every traversed file's canonical path to prevent escapes from the approved root. 5. Run the service under a dedicated, least-privileged account with access only to intended document and database directories. 6. Consider separating ingestion administration from search access. Ingestion should normally require stronger privileges than querying. 7. Keep loopback binding mandatory unless authentication, transport security, and network access controls have been configured. 8. Avoid returning internal database and source paths unless clients explicitly require them. 9. Add security tests for absolute paths, `..` traversal, symbolic-link escapes, unauthenticated requests, and non-loopback deployment. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/rag-service.mjs:45
Finding
Unbounded Request Bodies and Ingestion Work Permit Denial of Service<![CDATA[ ## Vulnerability Details **File Location**: `scripts/rag-service.mjs:45-49`, `scripts/rag-service.mjs:64-76`, and `scripts/rag-service.mjs:122-151` **Vulnerability Type**: Uncontrolled resource consumption **Risk Level**: Medium ### Vulnerable Code ```js async function readJsonBody(req) { const chunks = []; for await (const c of req) chunks.push(c); const raw = Buffer.concat(chunks).toString('utf8') || '{}'; return JSON.parse(raw); } ``` ```js function listTextFiles(dir) { if (!fs.existsSync(dir)) return []; const out = []; const stack = [path.resolve(dir)]; while (stack.length) { const cur = stack.pop(); for (const ent of fs.readdirSync(cur, { withFileTypes: true })) { const p = path.join(cur, ent.name); if (ent.isDirectory()) stack.push(p); else if (/\.(txt|md)$/i.test(ent.name)) out.push(p); } } return out; } ``` ```js async function ingestFromDir(dir, reset = true) { const files = listTextFiles(dir); if (!files.length) return { files: 0, chunks: 0 }; const rows = []; for (const f of files) { const text = fs.readFileSync(f, 'utf8'); const chunks = chunkText(text); chunks.forEach((c, idx) => rows.push({ id: `${path.basename(f)}#${idx + 1}`, source: path.resolve(f), chunkIndex: idx + 1, text: c, })); } if (!rows.length) return { files: files.length, chunks: 0 }; const firstVec = await embed(rows[0].text); ensureCollection(firstVec.length, reset); const docs = [{ id: rows[0].id, vectors: { embedding: firstVec }, fields: { source: rows[0].source, chunkIndex: rows[0].chunkIndex, text: rows[0].text }, }]; for (let i = 1; i < rows.length; i++) { const vec = await embed(rows[i].text); docs.push({ id: rows[i].id, vectors: { embedding: vec }, fields: { source: rows[i].source, chunkIndex: rows[i].chunkIndex, text: rows[i].text }, }); } ``` ### Technical Analysis `readJsonBody()` buffers the compl ...[truncated 2256 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce a small maximum HTTP body size while streaming the request. Abort and return HTTP `413 Payload Too Large` as soon as the limit is exceeded. 2. Configure header, request, idle, and overall operation timeouts. 3. Limit concurrent ingestion jobs and embedding requests. Consider allowing only one authenticated administrative ingestion job at a time. 4. Establish server-side limits for directory depth, file count, individual file size, total bytes, generated chunks, and total embeddings per request. 5. Reject ingestion before processing when preflight enumeration exceeds configured limits. 6. Stream or incrementally process files rather than retaining every file, chunk, vector, and document in memory simultaneously. 7. Replace synchronous filesystem and database operations on the request path where supported, or move ingestion to a bounded worker queue. 8. Add cancellation support so disconnected clients and expired jobs stop consuming resources. 9. Apply per-client rate limits and operating-system resource constraints. 10. Add tests covering oversized bodies, slow clients, very large files, deeply nested directories, and excessive file counts. ]]>

T08 · Insecure Dependencies

Warning
Location
package.json:5
Finding
Runtime Installation of an Unlocked Third-Party Dependency<![CDATA[ ## Vulnerability Details **File Location**: `package.json:5-6` and `scripts/manage.sh:24-33` **Vulnerability Type**: Non-reproducible dependency installation and supply-chain exposure **Risk Level**: Medium ### Vulnerable Code ```json "dependencies": { "@zvec/zvec": "^0.2.0" } ``` ```bash ensure_deps() { ensure_dirs if [ ! -f "$SKILL_DIR/package.json" ]; then echo "Missing package.json in skill directory" >&2 exit 1 fi # Explicit dependency install (declared in skill metadata) if [ ! -d "$SKILL_DIR/node_modules/@zvec/zvec" ]; then npm install --prefix "$SKILL_DIR" fi } ``` ### Technical Analysis The package declares `@zvec/zvec` using the floating range `^0.2.0`, while the audited project contains no lockfile. The bootstrap process invokes `npm install`, causing npm to resolve the dependency and its transitive dependency graph at installation time. Consequently, the installed code can differ from what was available during the audit. npm installation may also execute package lifecycle scripts or install native components with the privileges of the user running `manage.sh`. This is a supply-chain hardening deficiency; the audit did not identify evidence that the named package is itself malicious. ### Attack Path 1. A user executes `scripts/manage.sh bootstrap`, `start`, or `install-launchd` without an existing local installation. 2. `ensure_deps()` invokes `npm install --prefix "$SKILL_DIR"`. 3. npm resolves the newest versions permitted by `^0.2.0` and any ranges in transitive dependencies. 4. If an upstream account, package release, registry response, or transitive dependency is compromised, npm downloads the altered component. 5. Package lifecycle scripts or subsequently imported package code execute with the invoking user's privileges. 6. A compromised component could access user-readable files, modify project or user files, communicate over the network, or affect the long-running RAG service. ### Impact Assessmen ...[truncated 583 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin reviewed dependencies to exact versions instead of permissive ranges. 2. Generate, review, and commit a `package-lock.json`. 3. Replace runtime `npm install` with `npm ci` so installation fails if dependency metadata differs from the reviewed lockfile. 4. Review package integrity hashes and ensure installation uses the expected npm registry. 5. Disable lifecycle scripts with `--ignore-scripts` when they are unnecessary. If native or lifecycle installation is required, document and review the scripts explicitly. 6. Run dependency installation and the service under a dedicated, least-privileged account. 7. Use automated vulnerability, provenance, and package-signature checks where available. 8. Periodically update dependencies through a controlled review process rather than resolving new versions automatically during service startup. ]]>
Vulnerability Patterns
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (44)

Ae1

High
Category
analysis-evasion
Content
- `scripts/rag-service.mjs` → HTTP service implementation
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Chaining Abuse

High
Category
Tool Misuse
Content
}

stop_manual() {
  if is_running; then kill "$(cat "$PID_FILE")"; rm -f "$PID_FILE"; echo "Stopped service"; else echo "Service is not running"; fi
}

write_plist() {
Confidence
75% confidence
Finding
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Chaining Abuse

High
Category
Tool Misuse
Content
}

stop_manual() {
  if is_running; then kill "$(cat "$PID_FILE")"; rm -f "$PID_FILE"; echo "Stopped service"; else echo "Service is not running"; fi
}

write_plist() {
Confidence
75% confidence
Finding
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill clearly instructs the user to run shell commands and manage a local service, but it does not declare any tool scope such as allowed shell usage. That mismatch weakens security boundaries because an agent or reviewer cannot tell from the manifest that the skill requires command execution and persistence-related operations.

Session Persistence

Medium
Category
Rogue Agent
Content
- `scripts/rag-service.mjs` → HTTP service implementation
- `scripts/manage.sh` → bootstrap/start/stop/restart/health/ingest/search
- `references/launchd.plist.template` → macOS LaunchAgent template

## Prerequisites
Confidence
95% confidence
Finding
The skill explicitly includes a launchd LaunchAgent template, which is a persistence mechanism that causes code to keep running across sessions. Even though this is aligned with the skill's purpose, persistence materially increases risk because a compromised or misconfigured service would survive reboots and continue exposing local HTTP functionality.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
scripts/manage.sh bootstrap
scripts/manage.sh install-launchd   # writes plist, inspect once
scripts/manage.sh start
scripts/manage.sh health
```
Confidence
96% confidence
Finding
The quick-start instructions direct the user to install a launchd agent, which establishes persistent execution on macOS. Persistence is security-relevant because it extends the lifetime of the service and any associated exposure, making accidental or malicious misuse harder to notice and remove.

Session Persistence

Medium
Category
Rogue Agent
Content
# 3) verify health
scripts/manage.sh health

# 4) create tiny test corpus
mkdir -p ./docs
cat > ./docs/sample.md <<'EOF'
Zvec + Ollama enables local semantic search.
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.