Back to skill

Security audit

Memory Router

Security checks for vulnerabilities and agentic risk

Overview

MemoryRouter is a local memory-management skill, but its destructive memory restore and tiering safety guarantees are weaker than the documentation suggests.

Review carefully before installing. Use only on a backed-up workspace, avoid custom path settings with absolute paths or .. components, run dry-run/status/audit first, and do not rely on the skill as the only backup before using --restore --force or any destructive memory operation.

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
memory-router.js:96
Finding
Destructive tiering continues after backup or archive write failure<![CDATA[ ## Vulnerability Details **File Location**: `memory-router.js`, lines 96–128 and 467–527 **Vulnerability Type**: Unchecked filesystem operation failure **Risk Level**: High ### Vulnerable Code ```js function writeSafe(filepath, content) { const resolved = path.resolve(filepath); // Path validation — reject paths outside workspace const validation = validatePath(filepath); if (!validation.ok) { console.error(`[memory-router] Refusing to write: ${validation.reason}`); return; } // Symlink check — must be before any file operations let fileExists = false; try { const lstat = fs.lstatSync(resolved); if (lstat.isSymbolicLink()) { console.error(`[memory-router] Refusing to write to symlink: ${filepath}`); return; } fileExists = true; } catch { // File doesn't exist yet — that's fine, we're creating it } // Size check if (content.length > MAX_FILE_SIZE) { console.error(`[memory-router] Content too large (${content.length} bytes) for ${filepath}`); return; } // Atomic write: write to temp file, then rename ensureDir(path.dirname(filepath)); const tmpPath = resolved + '.tmp-' + process.pid; try { fs.writeFileSync(tmpPath, content, 'utf8'); fs.renameSync(tmpPath, resolved); } catch (err) { // Clean up temp file on failure try { fs.unlinkSync(tmpPath); } catch {} console.error(`[memory-router] Write failed for ${filepath}: ${err.message}`); } } ``` ```js writeSafe(backupFile, backupContent); console.log(`[memory-router] ✅ Pre-tier backup: ${backupFile} (${lineCount} lines saved)`); // ─── Actual tiering ─── const archiveDir = path.join(WORKSPACE, config.tiering.archiveDir); ensureDir(archiveDir); const archiveFile = path.join(archiveDir, `MEMORY-archive-${getToday()}.md`); let archiveContent = `# Archived Memory Sections\n\n`; archiveContent += `> Auto-archived on ${getToday()} by memory-router skill\n`; archiveContent += `> Original MEMORY.md had ...[truncated 3081 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make `writeSafe()` return an explicit boolean or structured result: ```js return { ok: true, path: resolved }; ``` Return `{ ok: false, error }` for every validation or filesystem failure. 2. Abort tiering immediately unless the backup write succeeds: ```js const backupResult = writeSafe(backupFile, backupContent); if (!backupResult.ok) { throw new Error(`Tiering aborted: backup failed: ${backupResult.error}`); } ``` 3. Verify the backup after writing: - Confirm that it is a regular file and not a symlink. - Read it back. - Compare its byte length or a cryptographic hash with the intended backup content. 4. Require successful archive creation and verification before replacing `MEMORY.md`. 5. Perform the operation transactionally: - Write backup, archive, and new core content to temporary files. - Flush and validate all temporary files. - Commit the archive first and replace `MEMORY.md` last. - Roll back or retain the original file if any stage fails. 6. Do not print success messages until the corresponding operation has been verified. 7. Account for generated metadata and formatting when applying size limits, or use separate, appropriately bounded limits for input files and generated backup/archive files. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
memory-router.js:467
Finding
Configuration-controlled directory paths are created before workspace-boundary validation<![CDATA[ ## Vulnerability Details **File Location**: `memory-router.js`, lines 467–493 **Vulnerability Type**: Path traversal through unsafe directory creation **Risk Level**: Medium ### Vulnerable Code ```js // ─── Pre-tier backup (only after confirm, before actual tiering) ─── const backupDir = path.join(WORKSPACE, config.tiering.backupDir || 'memory/backups'); ensureDir(backupDir); const backupFile = path.join(backupDir, `MEMORY-backup-${getToday()}-${Date.now()}.md`); // Create backup with full content + unambiguous delimiter for safe restore const backupContent = `# MEMORY.md — Pre-Tier Backup `; backupContent += `> Created ${getToday()} ${new Date().toISOString()} by memory-router skill `; backupContent += `> Original: ${lineCount} lines, ${charCount} chars `; backupContent += `> This is a full backup before tiering. Do NOT delete this file. `; backupContent += `--- `; backupContent += `---END-METADATA--- `; backupContent += content; writeSafe(backupFile, backupContent); console.log(`[memory-router] ✅ Pre-tier backup: ${backupFile} (${lineCount} lines saved)`); // ─── Actual tiering ─── const archiveDir = path.join(WORKSPACE, config.tiering.archiveDir); ensureDir(archiveDir); const archiveFile = path.join(archiveDir, `MEMORY-archive-${getToday()}.md`); ``` The directory helper performs recursive creation without validation: ```js function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } ``` ### Technical Analysis The `backupDir` and `archiveDir` values are loaded from `config.json` and joined with the workspace path. Traversal components such as `../../outside` can cause the normalized path to escape the workspace. Although `writeSafe()` validates the eventual file path and rejects writes outside the workspace, `ensureDir()` is called first and does not perform equivalent validation. Recursive `mkdirSync()` can therefore create one or more directories outside the intended workspace boundary before the subse ...[truncated 1584 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate configured directories before calling `existsSync()`, `mkdirSync()`, `readdirSync()`, or any other filesystem operation. 2. Replace `ensureDir()` with a workspace-bound helper: ```js function ensureWorkspaceDir(dir) { const resolved = path.resolve(dir); const validation = validatePath(resolved); if (!validation.ok) { throw new Error(`Unsafe directory path: ${validation.reason}`); } fs.mkdirSync(resolved, { recursive: true }); return resolved; } ``` 3. Require `archiveDir`, `backupDir`, `manifestPath`, and `reportPath` to be relative paths. 4. Reject traversal components and absolute paths during configuration loading: ```js if (path.isAbsolute(value) || value.split(/[\\/]+/).includes('..')) { throw new Error('Configured path must remain inside the workspace'); } ``` 5. Resolve the workspace with `fs.realpathSync()` and verify the real parent path before creating directories. This provides stronger protection against symlinked parent components. 6. Apply the same centralized validation to all configuration-derived paths, including restore, manifest, audit-report, and archive enumeration paths. 7. Validate configuration at process startup and terminate before performing any operation if a path falls outside the allowed workspace. ]]>
Vulnerability Patterns
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Session Persistence

Medium
Category
Rogue Agent
Content
## Quick Install (1 minute)

```bash
# 1. Create the skill directory
mkdir -p ~/.openclaw/workspace/skills/memory-router

# 2. Copy the skill files
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.

Session Persistence

Medium
Category
Rogue Agent
Content
### "No MEMORY.md found"

Create one:
```bash
echo "# MEMORY.md - Long-Term Memory" > ~/.openclaw/workspace/MEMORY.md
```
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.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The design principles claim the tool generates reports and that 'nothing is silently deleted'. Earlier safety documentation explicitly states that archived files older than `retentionDays` are candidates for deletion and that this deletion is irreversible. That is an active contradiction in the skill's own intent and safety messaging.

Intent-Code Divergence

Medium
Confidence
96% confidence
Finding
The design principles state the system is 'Reversible' because original content is preserved in archives. However, the retention section explicitly warns that archived files may be deleted irreversibly after the configured retention period. This directly contradicts the claim that the system is reversible.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The comments at L0479 and L0497 describe creating a full pre-tier backup and then proceeding with actual tiering, but the code declares `const backupContent` and then repeatedly appends to it, which would throw at runtime on the first reassignment. This means the documented safety backup flow does not occur as described, creating a direct contradiction between the inline intent and actual executable behavior.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
This code performs a destructive file overwrite of MEMORY.md during restore. Although it logs warnings and requires a --force flag, there is no interactive confirmation prompt before the irreversible write, which is a weaker safeguard for a safety-critical operation affecting user data.

Intent-Code Divergence

Low
Confidence
77% confidence
Finding
The heartbeat guidance states that `--tier --dry-run` is 'side-effect free (no files created)'. Elsewhere, the configuration and feature description say tiering can auto-generate `memory/memory-manifest.json` via `generateOnTier`, which creates ambiguity about whether tier-related commands may write generated output. This is a documentation-level contradiction about operational side effects.

Static analysis

No suspicious patterns detected.