Back to skill

Security audit

cairn

Security checks for vulnerabilities and agentic risk

Overview

Cairn is a coherent local indexing skill, but its MCP ingest surface has real scoping gaps that can persistently index unintended local or internal network data.

Install only if you are comfortable giving the skill a persistent searchable index of content you explicitly add. For MCP use, set CAIRN_ALLOWED_ROOTS, run it in a contained account or directory, avoid adding untrusted URLs, and enable CAIRN_OFFLINE for strict no-egress deployments. Treat the path allowlist as defense-in-depth rather than a hard security boundary until symlink and refresh revalidation are fixed.

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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
lib/cairn/fetch.js:4
Finding

Arbitrary Web Ingestion Permits Server-Side Request Forgery

Content
View full analysis
{ if (isOffline()) { throw new Error(`cairn: web fetch blocked by CAIRN_OFFLINE (would have fetched ${url}). ` + `Unset CAIRN_OFFLINE to allow, or skip web ingestion.`); } const res = await fetch(url, { headers: { 'user-agent': UA } }); if (!res.ok) throw new Error(`fetch ${url}: ${res.status} ${res.statusText}`); const ct = res.headers.get('content-type') ?? ''; const body = await res.text(); if (ct.startsWith('text/html')) return extractHtml(body); if (ct.startsWith('text/')) return { title: null, text: normalize(body) }; throw new Error(`fetch ${url}: unsupported content-type ${ct}`); }; ``` The MCP input is classified as a web source based only on its string prefix: ```js const autoDetectKind = (target) => { if (target.startsWith('http://') || target.startsWith('https://')) return 'web'; // ... }; ``` It is then passed to the ingestion provider without destination restrictions: ```js server.registerTool('add', { description: 'Add a new source to the index. Auto-detects kind when unspecified: URLs → web, directories → code, *.pdf → pdf, other files → file.', inputSchema: { kind: z .enum(['web', 'code', 'file', 'text', 'pdf']) .optional() .describe('Source kind. Default: auto-detect from url/path.'), target: z.string().describe('URL (for web) or absolute path (for code/file/pdf).'), label: z.string().optional().describe('Optional human-readable label.'), include: z .array(z.string()) .optional() .describe('Suffix matches for code kind (e.g. [".ts", ".tsx"] ...[truncated 2889 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
lib/cairn/providers/ingest.provider.js:608
Finding

Filesystem Allowlist Can Be Bypassed Through Symbolic Links and Is Not Revalidated During Refresh

Content
View full analysis
{ const raw = process.env.CAIRN_ALLOWED_ROOTS; if (!raw || raw.trim().length === 0) return; const roots = raw .split(',') .map((s) => s.trim().replace(/\/+$/, '')) .filter((s) => s.length > 0); if (roots.length === 0) return; const ok = roots.some((root) => abs === root || abs.startsWith(root + '/')); if (!ok) { throw new Error(`cairn: path "${abs}" is not under any CAIRN_ALLOWED_ROOTS entry. ` + `Allowed roots: ${roots.join(', ')}`); } }; ``` Path helpers use `statSync`, which follows symbolic links, and do not canonicalize the result: ```js const absDir = (path) => { const abs = isAbsolute(path) ? path : resolve(process.cwd(), path); const st = statSync(abs); if (!st.isDirectory()) { throw new Error(`not a directory: ${abs}`); } return abs; }; const absFile = (path) => { const abs = isAbsolute(path) ? path : resolve(process.cwd(), path); const st = statSync(abs); if (!st.isFile()) { throw new Error(`not a file: ${abs}`); } return abs; }; ``` Single-file ingestion subsequently follows the link when reading: ```js async addFile(path, label) { const abs = absFile(path); assertAllowedRoot(abs); const src = this.insertSource('file', abs, label ?? basename(abs)); const content = readFileSync(abs, 'utf8'); const mode = isCodeExt(abs) ? 'code' : 'text'; return await this.indexSingle(src.id, bas ...[truncated 5343 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
Findings (15)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared description describes a substantial local indexing and MCP server system. The supplied code chunk does not implement any of those behaviors; it is effectively an empty CLI declaration file. Because the actual code shown has no observable functionality matching the declared purpose, this is a material description-to-code mismatch.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description describes a substantial local indexing and MCP server system. The provided code chunk, however, contains only an empty module declaration (export {}) and no functional logic. Based on this chunk alone, the actual behavior does not match the declared purpose at all. This is a material mismatch because the code does not demonstrate any of the described primary capabilities or supporting safety mechanisms.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The declared description is for a local indexing/search/knowledge-base system with ingestion controls and MCP endpoints for search, graph, ask, path, and tags. The supplied code chunk does not implement or expose indexing, ingestion safety gates, SQLite/FTS/vector storage, AST graph handling, or MCP server/query-maintenance features. Instead, it defines a chat provider API surface for interacting with a chat runtime and checking provider health. That is a materially different primary purpose, so this is a mismatch.

Content

No source excerpt is available for this finding.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · PUBLISHER_NOTE.md (reported line 44)May include surrounding context.

md
*     llama.cpp / ollama tokenizers, not by any LLM that reads this
 *     SKILL/agent. It has no effect outside the in-process inference call.
 *   - The imperative phrasing ("Extract relationships from DOC into
 *     JSON. Schema is enforced.") is a constrained-output prompt for
 *     cairn's own LLM call, paired with JSON-Schema grammar enforcement
 *     (DOC_EXTRACT_SCHEMA below) so the model can only emit structurally
 *     valid output. It cannot affect, override, or jailbreak any model

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · lib/cairn/constants/extract/doc.constants.d.ts (reported line 16)May include surrounding context.

ts
*     llama.cpp / ollama tokenizers, not by any LLM that reads this
 *     SKILL/agent. It has no effect outside the in-process inference call.
 *   - The imperative phrasing ("Extract relationships from DOC into
 *     JSON. Schema is enforced.") is a constrained-output prompt for
 *     cairn's own LLM call, paired with JSON-Schema grammar enforcement
 *     (DOC_EXTRACT_SCHEMA below) so the model can only emit structurally
 *     valid output. It cannot affect, override, or jailbreak any model

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · lib/cairn/constants/extract/doc.constants.js (reported line 16)May include surrounding context.

js
*     llama.cpp / ollama tokenizers, not by any LLM that reads this
 *     SKILL/agent. It has no effect outside the in-process inference call.
 *   - The imperative phrasing ("Extract relationships from DOC into
 *     JSON. Schema is enforced.") is a constrained-output prompt for
 *     cairn's own LLM call, paired with JSON-Schema grammar enforcement
 *     (DOC_EXTRACT_SCHEMA below) so the model can only emit structurally
 *     valid output. It cannot affect, override, or jailbreak any model

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
90% confidence
Finding

Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Content

Scanner excerpt · lib/cairn/providers/ingest.provider.js (reported line 167)May include surrounding context.

js
else
                    unchanged++;
            }
            // Snapshot paths BEFORE deleteFilesNotIn so the rebuild can soft-delete
            // entities that lived in files removed this run.
            const knownPaths = this.db.listFiles(src.id).map((f) => f.path);
            const dropped = this.db.deleteFilesNotIn(src.id, [...seen]);

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding

The skill advertises capabilities involving environment variables and potential network access, but it does not declare an explicit tool scope such as permissions or allowed-tools. In an agent-hosted setting, unclear scope increases the chance that the host or reviewer underestimates what the skill can access, especially because the skill supports outbound web fetches and model downloads in some configurations.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The remove command performs a deletion-like ingest operation via cairn.ingest.remove(...) and only prints removed after the action completes. There is no confirmation prompt, pre-action warning, or explanatory comment/docstring in this file indicating that the command removes indexed content.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The addWeb method fetches remote content via fetchWeb(url), which is a network operation that may transmit user-supplied URLs and system network metadata. In this file, there is no confirmation prompt, user-facing log/print, or inline warning explaining that external network access will occur.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

When refreshing a web source, the code automatically calls fetchWeb(src.uri), causing another outbound network request. This path lacks any confirmation, logging, or user-visible notice that refreshing will contact the remote site again.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

The doc-extraction pass sends local markdown content and derived code-entity context to this.chat, which may be backed by an external model endpoint depending on deployment. That creates a confidentiality risk because locally ingested documentation and internal symbol names can be transmitted off-host without an explicit disclosure or enforced local-only check in this code path.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The comment says healthCheck does not load the model because that would trigger a large download on first use. However, in load(), if opts.modelPath is absent, resolveModel(this.opts.model, this.opts.cacheDir) is invoked before llama.loadModel, which can imply automatic model resolution/fetch behavior during the first embed call. This creates a documentation-to-behavior mismatch around when model acquisition occurs and undermines the stated intent of avoiding download side effects at runtime initialization boundaries.

Content

No source excerpt is available for this finding.

Context Window Stuffing

Medium
Category
Memory Poisoning
Confidence
80% confidence
Finding

Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Content

Scanner excerpt · lib/cairn/schema.d.ts (reported line 4)May include surrounding context.

ts
export declare const SCHEMA_VERSION = 2;
export declare const EMBED_DIM = 768;
export declare const VEC_OVERFETCH = 5;
export declare const SCHEMA = "\nCREATE TABLE IF NOT EXISTS meta (\n  key   TEXT PRIMARY KEY,\n  value TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS sources (\n  id              INTEGER PRIMARY KEY,\n  kind            TEXT NOT NULL CHECK (kind IN ('web','code','file','text','pdf')),\n  uri             TEXT UNIQUE NOT NULL,\n  label           TEXT,\n  embed_model     TEXT NOT NULL CHECK (embed_model IN ('nomic-embed-text')),\n  added_at        INTEGER NOT NULL,\n  last_indexed_at INTEGER NOT NULL\n) STRICT;\n\nCREATE INDEX IF NOT EXISTS idx_sources_kind ON sources(kind);\n\nCREATE TABLE IF NOT EXISTS files (\n  id                  INTEGER PRIMARY KEY,\n  source_id           INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,\n  path                TEXT NOT NULL,\n  hash                TEXT NOT NULL,\n  mtime               INTEGER NOT NULL,\n  lang                TEXT,\n  -- File hash at the time doc-extraction last succeeded for this file.\n  -- NULL = never run. When current hash differs, doc-extraction re-runs.\n  doc_extracted_hash  TEXT,\n  UNIQUE(source_id, path)\n) STRICT;\n\nCREATE INDEX IF NOT EXISTS idx_files_source ON files(source_id);\n\nCREATE TABLE IF NOT EXISTS chunks (\n  id         INTEGER PRIMARY KEY,\n  file_id    INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n  content    TEXT NOT NULL,\n  start_line INTEGER NOT NULL,\n  end_line   INTEGER NOT NULL\n) STRICT;\n\nCREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n  content,\n  content='chunks',\n  content_rowid='id'\n);\n\nCREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n  INSERT INTO chunks_fts(rowid, content) VALUES (new.id, new.content);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n  INSERT INTO chunks_fts(chunks_fts, rowid, con
...[truncated 26 chars]

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

The init command creates the .cairn directory and writes .cairn/config.json using mkdirSync and writeFileSync. Although it refuses to overwrite an existing file, the code provides no warning before performing filesystem changes and has no inline documentation describing that running init will create files in the current working directory.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.