Back to skill

Security audit

workspace-backup-manager

Security checks for vulnerabilities and agentic risk

Overview

This backup skill has a legitimate purpose, but its file restore and cleanup powers are broad enough to overwrite or delete important workspace data without adequate safeguards.

Review before installing. Use only in trusted workspaces, avoid passing untrusted backup names or backup directories, and do not let automated agents call restore or cleanup without explicit human confirmation. The skill should validate backup names and paths, reject symlinks, require valid backup metadata, and add preview or confirmation steps before restore or deletion.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
index.js:36
Finding

Backup Name Path Traversal Enables Writes Outside the Backup Directory

Content
View full analysis

Vulnerability Details

File Location: index.js, lines 36-42
Vulnerability Type: Unvalidated path component and directory traversal
Risk Level: High

javascript
// Generate backup name
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = name || `backup-${timestamp}`;
const backupPath = path.join(backupDir, backupName);

if (fs.existsSync(backupPath)) {
  return { success: false, error: 'Backup with this name already exists', ...result };
}

fs.mkdirSync(backupPath, { recursive: true });
result.backup_path = backupPath;

Technical Analysis

The caller-controlled name value is passed directly to path.join() without requiring it to be a single safe directory name. A value containing .. path segments can cause backupPath to resolve outside backupDir.

The subsequent backup process creates this directory and copies selected workspace files into it. These files can include MEMORY.md, SOUL.md, IDENTITY.md, AGENTS.md, USER.md, HEARTBEAT.md, and TOOLS.md, as well as content from the memory and logs directories.

Checking whether the resulting path already exists does not establish that it is inside the authorized backup directory.

Attack Path

  1. An attacker or untrusted caller invokes createBackup() with a crafted name, such as:
    javascript
    createBackup({ name: '../../attacker-output' });
    
  2. path.join(backupDir, name) resolves the traversal sequence outside the configured backup directory.
  3. fs.mkdirSync() creates the attacker-selected destination.
  4. The backup operation copies sensitive workspace files into that out-of-scope location.
  5. Anyone with access to the destination can read the copied workspace data.

Impact Assessment

An attacker able to control backup options can create directories and write backup data anywhere permitted by the process account. The exposed data may include identi ...[truncated 264 chars]

Remediation
View remediation

Remediation Suggestions

  • Require name to be a single basename with a strict allowlist, such as letters, digits, underscores, and hyphens.
  • Reject absolute paths, path separators, empty names, . entries, and .. entries.
  • Resolve both paths and verify containment before performing any write:
    javascript
    const root = path.resolve(backupDir);
    const destination = path.resolve(root, backupName);
    if (
      destination === root ||
      !destination.startsWith(root + path.sep)
    ) {
      throw new Error('Invalid backup name');
    }
    
  • Do not rely solely on prefix checks without appending path.sep, because sibling paths can share textual prefixes.
  • Use restrictive directory and file permissions when creating backups.
  • Add tests covering ../, nested traversal, absolute paths, alternate separators, empty names, and sibling-prefix bypasses.

T02 · Agent Memory Poisoning

Error
Location
index.js:208
Finding

Restore Path Traversal Allows Arbitrary Directories to Poison Workspace State

Content
View full analysis

Vulnerability Details

File Location: index.js, lines 208-229
Vulnerability Type: Arbitrary restore source and persistent workspace-state overwrite
Risk Level: Critical

javascript
try {
  const backupPath = path.join(backupDir, backupName);
  
  if (!fs.existsSync(backupPath)) {
    return { success: false, error: 'Backup not found', ...result };
  }

  // Restore files
  const entries = fs.readdirSync(backupPath, { withFileTypes: true });
  
  for (const entry of entries) {
    if (entry.name === 'backup-metadata.json') continue;
    
    const srcPath = path.join(backupPath, entry.name);
    const destPath = path.join(workspacePath, entry.name);
    
    if (entry.isDirectory()) {
      copyDir(srcPath, destPath, result);
    } else {
      fs.copyFileSync(srcPath, destPath);
      result.files_restored++;
    }
  }

Technical Analysis

backupName is used as a path component without validation or a canonical containment check. A name containing traversal segments can therefore select any readable directory as the restore source rather than a legitimate child of backupDir.

After selecting the source directory, the function copies all top-level entries other than backup-metadata.json into the workspace. It neither requires valid backup metadata nor limits restored entries to the files and directories documented by the Skill.

Consequently, an attacker-controlled directory containing files such as MEMORY.md, SOUL.md, AGENTS.md, or TOOLS.md can overwrite persistent workspace state. In an agent environment where these files influence later behavior, this creates a direct memory-poisoning path.

Attack Path

  1. The attacker creates or identifies a readable directory containing malicious workspace files, such as an AGENTS.md or MEMORY.md with attacker-controlled instructions.
  2. The attacker invokes restoreBackup() with a traversing name that resol ...[truncated 1137 chars]
Remediation
View remediation

Remediation Suggestions

  • Reject missing, non-string, absolute, nested, or traversal-containing backup names.
  • Resolve the restore source and verify that it is a direct child of the canonical backup root.
  • Require a valid backup-metadata.json with a recognized schema and expected backup identifier.
  • Restore only an explicit allowlist of documented files and directories.
  • Reject unexpected top-level entries, symbolic links, device files, sockets, and other non-regular entries.
  • Validate every recursively restored source and destination with canonical containment checks.
  • Prompt for explicit confirmation before overwriting sensitive files such as MEMORY.md, SOUL.md, AGENTS.md, or TOOLS.md.
  • Consider restoring into a staging directory first, validating the complete manifest, and then performing an atomic replacement.
  • Preserve or create a rollback snapshot before modifying persistent workspace state.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
index.js:255
Finding

Unrestricted Cleanup Can Recursively Delete Unrelated Directories

Content
View full analysis

Vulnerability Details

File Location: index.js, lines 255-276
Vulnerability Type: Unsafe recursive deletion and insufficient backup validation
Risk Level: High

javascript
function cleanupBackups(options = {}) {
  const {
    backupDir = '/root/.openclaw/workspace/backups',
    keepCount = 10
  } = options;

  const result = {
    deleted: [],
    kept: []
  };

  try {
    const listResult = listBackups(backupDir);
    if (!listResult.success) {
      return listResult;
    }

    const backups = listResult.backups;
    
    // Keep the most recent backups
    const toKeep = backups.slice(0, keepCount);
    const toDelete = backups.slice(keepCount);

    result.kept = toKeep.map(b => b.name);

    for (const backup of toDelete) {
      fs.rmSync(backup.path, { recursive: true });
      result.deleted.push(backup.name);
    }

Technical Analysis

cleanupBackups() accepts an unrestricted backupDir and recursively deletes entries classified as old backups. The associated listBackups() function treats every child directory as a backup, even when it has no valid metadata or recognized backup name.

keepCount is also not validated as a positive, bounded integer. A value of zero selects every listed directory for deletion. Negative or otherwise unexpected numeric values can produce unintuitive slice() behavior.

There is no final canonical-path check immediately before fs.rmSync(), and the deletion is irreversible within this function.

Attack Path

  1. An attacker or untrusted caller selects a parent directory containing unrelated subdirectories.
  2. The attacker invokes:
    javascript
    cleanupBackups({
      backupDir: '/path/to/sensitive-parent',
      keepCount: 0
    });
    
  3. listBackups() classifies each child directory as a backup without requiring valid metadata.
  4. slice(0) places all entries in toDelete.
  5. `fs.rmSyn ...[truncated 457 chars]
Remediation
View remediation

Remediation Suggestions

  • Restrict cleanup to a fixed, trusted backup root rather than accepting arbitrary directories from untrusted callers.
  • Canonicalize the configured root and every deletion target, then verify that each target is a direct child of that root.
  • Require each candidate to have valid backup metadata and a strictly generated backup-name format.
  • Validate keepCount with Number.isInteger() and enforce a safe range, such as 1 through a configured maximum.
  • Refuse to delete the backup root itself, filesystem roots, the workspace root, or any path outside the approved backup store.
  • Use a two-phase cleanup process that first returns the proposed deletion set and requires confirmation.
  • Consider moving expired backups into a quarantine directory before permanent deletion.
  • Log deletion decisions and failures without exposing sensitive filesystem details to untrusted users.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
index.js:110
Finding

Recursive Backup Copy Follows Symbolic Links Outside the Intended Source

Content
View full analysis

Vulnerability Details

File Location: index.js, lines 110-121
Vulnerability Type: Symbolic-link traversal during recursive copying
Risk Level: High

javascript
const entries = fs.readdirSync(src, { withFileTypes: true });

for (const entry of entries) {
  const srcPath = path.join(src, entry.name);
  const destPath = path.join(dest, entry.name);
  
  if (entry.isDirectory()) {
    copyDir(srcPath, destPath, result);
  } else {
    fs.copyFileSync(srcPath, destPath);
    result.files_backed_up++;
    result.size_bytes += fs.statSync(destPath).size;
  }
}

Technical Analysis

The recursive copy routine distinguishes directories from all other entry types using entry.isDirectory(). A symbolic link is not treated as a directory and therefore reaches the fs.copyFileSync() branch.

copyFileSync() operates on the linked target rather than preserving or rejecting the symbolic link. A link located inside a documented backup source such as memory/ or logs/ can consequently reference a readable file outside the workspace. The external file's contents are then copied into the backup.

The implementation does not call lstatSync(), reject symbolic links, require regular files, or verify that the canonical source remains below the authorized source root.

Attack Path

  1. An attacker who can write to memory/ or logs/ creates a symbolic link to a sensitive file readable by the backup process:
    text
    logs/external-secret -> /path/to/sensitive-file
    
  2. A normal backup is initiated.
  3. readdirSync(..., { withFileTypes: true }) returns the symbolic-link entry.
  4. Because the entry is not identified as a directory, the code passes it to copyFileSync().
  5. The linked target's contents are copied into the backup as external-secret.
  6. Anyone able to read the backup gains access to the copied data.

Impact Assessment

A user with write access to a b ...[truncated 474 chars]

Remediation
View remediation

Remediation Suggestions

  • Inspect each entry with fs.lstatSync() and reject symbolic links.
  • Copy only entries confirmed to be regular files or directories.
  • Resolve each source through fs.realpathSync() and verify that it remains inside the canonical authorized source root.
  • Apply equivalent validation at every level of recursive traversal.
  • Open source files with protections against symbolic-link substitution where supported, and account for time-of-check/time-of-use races.
  • If links must be supported, preserve them as links only after validating their targets and documenting the behavior; do not silently dereference them.
  • Add tests for links to files, links to directories, chained links, broken links, and links changed concurrently during backup.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • 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
Findings (5)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The skill documentation describes restore and cleanup operations that can overwrite the current workspace state or permanently delete older backups, but it provides no warning, confirmation requirement, or safety guidance. In a backup-management context, these actions are inherently destructive if misused, and the lack of caution increases the chance that an agent or user invokes them without understanding the consequences.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The restore operation copies files from a chosen backup directly into the workspace and will overwrite existing workspace content without any confirmation, dry-run, or conflict detection. In a skill running with access to important workspace state files, an accidental or manipulated restore can destroy current state, revert trusted files, or reintroduce unsafe content from an older backup.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The cleanup routine permanently removes backup directories with fs.rmSync(..., { recursive: true }) and does so without any user-facing warning, preview, or safety interlock. Because backups are the recovery mechanism, accidental invocation or misleading parameters can irreversibly erase restore points and increase the impact of later failures or compromise.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest describes creating snapshots and enabling restore points for recovery, which suggests backup creation and restoration. The code also implements cleanupBackups that permanently deletes backup directories, a lifecycle-management capability not stated in the manifest description.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

This manifest file describes the skill only as managing workspace backups and restore points, but does not specify when it should be invoked, what requests should trigger it, or any exclusions. In a manifest context, that lack of trigger specificity can make the skill eligible for unintended invocation on generic backup- or recovery-related requests.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.