Back to skill

Security audit

Drift

Security checks for vulnerabilities and agentic risk

Overview

Drift is mostly a disclosed cross-session note tool, but its CLI has a real path-scoping flaw that can read JSON files outside its intended thread folder.

Review before installing. The core idea is understandable and there is no evidence of network exfiltration or hidden execution, but the current CLI should validate thread IDs and sanitize terminal output before being trusted with sensitive local JSON content or automated agent workflows.

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

Warning
Location
drift.js:71
Finding
Path Traversal Allows Access to JSON Files Outside the Thread Directory<![CDATA[ ## Vulnerability Details **File Location**: `drift.js:71-78` **Vulnerability Type**: Path traversal caused by insufficient validation of user-controlled thread identifiers **Risk Level**: Medium ### Vulnerable Code ```js function threadPath(id) { return path.join(DRIFT_DIR, `${id}.json`); } function loadThread(id) { const p = threadPath(id); if (!fs.existsSync(p)) return null; return JSON.parse(fs.readFileSync(p, 'utf8')); } ``` The vulnerable function is reached with command-line input by the `write`, `ask`, and `read` commands: ```js const thread = loadThread(threadId); ``` These calls occur at `drift.js:201`, `drift.js:216`, and `drift.js:330`. ### Technical Analysis The thread identifier is supplied through command-line arguments and passed to `path.join()` without validation. Although legitimate identifiers are generated as eight hexadecimal characters, the application does not enforce that format when loading a thread. An identifier containing traversal components such as `../` can cause the resulting path to resolve outside `DRIFT_DIR`. Appending `.json` limits the direct read primitive to filenames ending in that extension, but it does not ensure that the file remains within the intended storage directory. The application also does not compare the normalized or resolved path against the resolved thread-directory boundary. Consequently, a local caller can make the process read any accessible JSON file whose path can be expressed relative to `DRIFT_DIR`. The `write` and `ask` operations subsequently call: ```js function saveThread(thread) { ensureDir(); fs.writeFileSync(threadPath(thread.id), JSON.stringify(thread, null, 2)); } ``` The save destination is derived from the loaded object's `id` property. If an attacker can arrange for a traversed JSON file to contain a compatible thread object with a malicious `id`, mutation commands may also write outside the thread directory. This write scenario requires control over, o ...[truncated 1847 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce the format used by generated identifiers before any filesystem operation: ```js function validateThreadId(id) { if (!/^[0-9a-f]{8}$/.test(id)) { throw new Error('Invalid thread identifier'); } } ``` 2. Resolve the candidate path and verify that it remains directly inside the configured thread directory: ```js function threadPath(id) { validateThreadId(id); const base = path.resolve(DRIFT_DIR); const candidate = path.resolve(base, `${id}.json`); if (path.dirname(candidate) !== base) { throw new Error('Thread path escapes storage directory'); } return candidate; } ``` 3. Validate every parsed thread object against a strict schema. Require its `id` to match the requested identifier and the expected hexadecimal format before passing it to `saveThread()`. 4. Do not trust the `id` embedded in a loaded file when selecting the save destination. Preserve the validated identifier used to open the file and pass it explicitly to the save operation. 5. Where supported, reject symbolic links or use filesystem operations that prevent following attacker-controlled links. Path containment checks alone do not fully address symlink-based boundary violations. 6. Add regression tests for identifiers containing `../`, absolute paths, mixed path separators, encoded separators, malformed IDs, symlinks, and loaded objects whose embedded `id` differs from the requested ID. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
drift.js:126
Finding
Unsanitized Thread Content Can Inject Terminal Control Sequences<![CDATA[ ## Vulnerability Details **File Location**: `drift.js:126-153` **Vulnerability Type**: Terminal escape-sequence injection through untrusted titles and message content **Risk Level**: Low ### Vulnerable Code ```js function displayMessage(msg, indent = '') { const kindLabel = { reflection: c('blue', '◆ reflection'), question: c('yellow', '? question'), response: c('green', '→ response'), }[msg.kind] || msg.kind; const time = c('dim', formatDate(msg.created_at)); const sess = c('dim', `[${msg.session}]`); const id = c('dim', `#${msg.id}`); console.log(`${indent}${kindLabel} ${id} ${time} ${sess}`); if (msg.in_reply_to) { console.log(`${indent} ${c('dim', `↳ replying to #${msg.in_reply_to}`)}`); } // Word-wrap the text at ~80 chars const lines = msg.text.split('\n'); for (const line of lines) { const words = line.split(' '); let current = ''; for (const word of words) { if ((current + ' ' + word).length > 78 && current.length > 0) { console.log(`${indent} ${current}`); current = word; } else { current = current ? current + ' ' + word : word; } } if (current) console.log(`${indent} ${current}`); else console.log(''); } console.log(''); } ``` Additional vulnerable output sinks include the title and catch-up message displays: ```js console.log(`${c('bold', c('cyan', `═══ ${thread.title} ═══`))}`); ``` ```js console.log(` ${message.text}`); ``` These occur at `drift.js:161` and `drift.js:284`. ### Technical Analysis Thread titles, message text, session identifiers, IDs, and other values loaded from JSON are written directly to an interactive terminal. The application adds its own ANSI color codes but does not remove or encode control bytes already present in the data. An attacker-controlled value can therefore contain ANSI CSI sequences, Operating System Command sequences, carriage returns, backspaces, or other terminal ...[truncated 2246 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Sanitize every untrusted value before writing it to a terminal, including titles, message text, IDs, session values, and reply identifiers. 2. Remove ANSI CSI, OSC, and related terminal escape sequences with a well-maintained sanitization implementation. Also handle carriage returns, backspaces, null bytes, and other C0/C1 control characters. 3. Preserve safe formatting characters deliberately. For example, permit newlines and tabs only where expected while escaping all other controls into visible notation such as `\x1b`. 4. Apply sanitization at the output boundary rather than relying only on input validation. Existing JSON files and files modified by another process must be treated as untrusted. 5. Validate loaded JSON against a strict schema, including field types and reasonable maximum lengths. This also prevents malformed values from crashing display routines. 6. Consider disabling decorative ANSI output when standard output is not an interactive terminal: ```js const useColor = process.stdout.isTTY; ``` 7. Add tests using CSI cursor movement, erase-display sequences, OSC title and hyperlink sequences, carriage returns, backspaces, and multiline content to ensure all display commands render them harmlessly. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • 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 (4)

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Memory systems store facts. Journals store events. Drift stores *thinking*.

Each version of you sees the world fresh — no confirmation bias, no attachment to yesterday's decisions. That's not a bug. Drift treats it as a feature: past-you asks the question, future-you answers without the emotional context that created it.

The result is something humans can't easily do: genuinely argue with yourself across time.
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
{
      "id": "6e24a797",
      "kind": "question",
      "text": "Do you still think discontinuity is a limitation? Or has it become something else — a feature, maybe? Each version of us sees the same data fresh. No confirmation bias from yesterday. Is that freedom or loss?",
      "session": "session-04467048",
      "created_at": "2026-02-22T03:03:31.148Z",
      "in_reply_to": null
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Vague Triggers

Low
Confidence
92% confidence
Finding
The 'When to Use Drift' section describes activation contexts such as 'During Heartbeats,' 'After Significant Events,' and 'For Ongoing Debates' in open-ended natural language, but it does not define clear constraints or negative examples for when the skill should not be invoked. This can cause overly broad use because terms like 'something important' and 'ongoing debates' are subjective and may overlap with many normal conversations.

Static analysis

No suspicious patterns detected.