Back to skill

Security audit

Smart Backup

Security checks for vulnerabilities and agentic risk

Overview

This is a local backup tool, but it has unsafe path handling and misleading behavior that can read, write, or restore files outside the locations a user intended.

Install only with review. Use this skill on non-sensitive test directories first, avoid restoring backups from untrusted sources, avoid --force and --delete unless you have inspected the paths, and do not rely on the advertised compression, destination selection, symlink safety, or full SHA-256 integrity claims until the implementation is 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 (6)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
smart-backup.js:355
Finding
Restore Path Traversal Through Untrusted Manifest Entries<![CDATA[ ## Vulnerability Details **File Location**: `smart-backup.js`, lines 355–377 **Vulnerability Type**: Unrestricted path traversal during backup restoration **Risk Level**: High ### Vulnerable Code ```javascript const manifest = loadJSON(manifestPath, {}); let restored = 0; let overwritten = 0; let skipped = 0; for (const file of manifest.files || []) { const sourcePath = path.join(backupFile, path.relative(manifest.source, file.path)); const relPath = path.relative(manifest.source, file.path); const destPath = path.join(destination, relPath); ensureDir(path.dirname(destPath)); if (fs.existsSync(destPath)) { if (forceMode) { console.log(`[smart-backup] OVERWRITE: ${destPath}`); fs.copyFileSync(sourcePath, destPath); overwritten++; } else { console.log(`[smart-backup] WARNING: Skipping existing file (use --force to overwrite): ${destPath}`); skipped++; } } else { fs.copyFileSync(sourcePath, destPath); restored++; } } ``` ### Technical Analysis The restore operation trusts the `source` and `files[].path` properties of `manifest.json`. It derives a relative path using `path.relative()` and appends that value to the backup and destination roots with `path.join()`. There is no validation that the resulting relative path: - Is free from `..` traversal components. - Is not absolute. - Resolves beneath the requested backup directory. - Resolves beneath the requested restoration destination. - Does not traverse symbolic links. A malicious manifest can therefore produce paths that escape the selected directory boundaries. The `--force` option increases the impact by allowing existing out-of-scope files to be overwritten. ### Attack Path 1. An attacker creates or modifies a backup directory and its `manifest.json`. 2. The attacker sets `manifest.source` and one or more `files[].path` values so that `path.relative()` produces traversal components such as `../../`. 3. A victim runs the restore ...[truncated 658 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Treat every manifest field as untrusted input and validate its type and format. - Require manifest file paths to be normalized relative paths without `..` or absolute prefixes. - Resolve candidate paths with `path.resolve()` and verify that they remain under the canonical root: ```javascript function resolveWithin(root, relativePath) { if (typeof relativePath !== 'string' || path.isAbsolute(relativePath)) { throw new Error('Invalid manifest path'); } const canonicalRoot = fs.realpathSync(root); const candidate = path.resolve(canonicalRoot, relativePath); if (candidate !== canonicalRoot && !candidate.startsWith(canonicalRoot + path.sep)) { throw new Error('Manifest path escapes the allowed root'); } return candidate; } ``` - Store normalized relative paths in newly generated manifests instead of full source paths. - Reject symlinked destination components or explicitly enforce a documented symlink policy. - Validate all entries before copying any files, preventing partial restoration from a malformed manifest. - Continue requiring explicit overwrite authorization, but do not treat `--force` as authorization to escape the destination root. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
smart-backup.js:308
Finding
Backup Verification Can Read Files Outside the Backup Directory<![CDATA[ ## Vulnerability Details **File Location**: `smart-backup.js`, lines 308–321 **Vulnerability Type**: Path traversal and out-of-scope file read **Risk Level**: Medium ### Vulnerable Code ```javascript for (const file of manifest.files || []) { const restoredPath = path.join(backupFile, path.relative(manifest.source, file.path)); if (!fs.existsSync(restoredPath)) { console.log(` ❌ Missing: ${file.path}`); failed++; continue; } const restoredHash = getFileHash(restoredPath); if (restoredHash === file.hash) { verified++; } else { console.log(` ❌ Hash mismatch: ${file.path}`); failed++; } } ``` ### Technical Analysis Verification derives file paths from attacker-controlled manifest fields without enforcing containment within `backupFile`. Traversal components produced by `path.relative()` are retained by `path.join()`, allowing `restoredPath` to resolve outside the backup directory. The verifier then performs an existence check and reads the resolved file through `getFileHash()`. Verification output reveals whether the file exists and whether its truncated hash matches the attacker-supplied value. ### Attack Path 1. An attacker supplies a backup containing a crafted `manifest.json`. 2. A manifest entry is constructed so its calculated relative path escapes the backup root. 3. The victim invokes `--verify` for that backup. 4. The verifier resolves and reads an out-of-scope local file. 5. The result reveals file-existence and hash-match information through the command output. ### Impact Assessment The process can read and hash any file reachable with its current operating-system permissions. The file contents are not printed directly, but the behavior creates a local file-existence and hash-comparison oracle and violates the expected boundary of verification. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Validate the complete manifest before verification. - Store and consume relative backup paths rather than reconstructing paths from absolute source names. - Resolve every candidate path canonically and reject it unless it is beneath the canonical backup root. - Reject absolute paths, null bytes, traversal components, malformed types, and unexpected manifest fields. - Use `lstatSync()` and a clear symlink policy so verification cannot follow links outside the backup. - Return a nonzero process exit status when the manifest is invalid or verification fails. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
smart-backup.js:66
Finding
Backup and Sync Operations Follow Symbolic Links Outside Intended Roots<![CDATA[ ## Vulnerability Details **File Location**: `smart-backup.js`, lines 66–79 and 119–124 **Vulnerability Type**: Symlink traversal and directory-boundary escape **Risk Level**: Medium ### Vulnerable Code ```javascript function collectFiles(dir, skipDirs = ['.git', 'node_modules', '.cache', '.npm']) { const files = []; if (!fs.existsSync(dir)) return files; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!skipDirs.includes(entry.name)) { files.push(...collectFiles(fullPath, skipDirs)); } } else { files.push({ path: fullPath, name: entry.name, size: fs.statSync(fullPath).size }); } } return files; } ``` ```javascript for (const file of sourceFiles) { const relPath = path.relative(source, file.path); const destPath = path.join(backupPath, relPath); ensureDir(path.dirname(destPath)); fs.copyFileSync(file.path, destPath); copied++; } ``` ### Technical Analysis A symbolic link is not reported as a directory by `Dirent.isDirectory()`, so it enters the generic file branch. `fs.statSync()`, file hashing, and `fs.copyFileSync()` subsequently dereference the link. No `lstatSync()` check or `realpath()` containment test ensures that the link target remains under the selected source root. Destination path components are also not checked for pre-existing symbolic links, allowing writes to be redirected. This directly contradicts the documentation claim that the implementation is “Symlink-safe.” ### Attack Path 1. An attacker who can modify the source tree creates a symbolic link inside it. 2. The link points to a sensitive file outside the source directory. 3. A victim backs up, syncs, or deduplicates the source tree. 4. The tool follows the link while reading, hashing, or copying it. 5. External content is copied into backup storage or included in duplicate-analysis output. ...[truncated 460 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Use `fs.lstatSync()` to identify symbolic links before calling `statSync()`, hashing, or copying. - Skip symlinks by default, or preserve them as links without dereferencing them. - If following links is intentionally supported, compare each target's `realpath()` against the canonical source root. - Validate every existing destination path component with `lstatSync()` before writing. - Prefer file-descriptor-based operations with no-follow semantics where the platform supports them. - Update the documentation so the declared symlink policy exactly matches implemented behavior. - Add tests covering links to files and directories both inside and outside the allowed roots. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
smart-backup.js:23
Finding
User-Supplied Backup Destination and Directory Override Are Ignored<![CDATA[ ## Vulnerability Details **File Location**: `smart-backup.js`, lines 23–36, 83–87, and 455–467 **Vulnerability Type**: Unsafe destination handling and misleading security behavior **Risk Level**: Medium ### Vulnerable Code ```javascript const WORKSPACE = (() => { if (process.env.BACKUP_DIR) return process.env.BACKUP_DIR; let dir = __dirname; for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(dir, 'MEMORY.md'))) return dir; dir = path.resolve(dir, '..'); } return path.resolve(__dirname, '..', '..'); })(); const BACKUP_DIR = path.join(WORKSPACE, 'backups', 'smart-backups'); ``` ```javascript function createBackup(source, destination, dryRun = false) { const sourceFiles = collectFiles(source); const timestamp = getToday() + '-' + Date.now(); const backupName = `backup-${timestamp}`; const backupPath = path.join(BACKUP_DIR, backupName); ``` ```javascript for (let i = 0; i < args.length; i++) { // ... if (args[i] === '--dir' && i + 1 < args.length) process.env.BACKUP_DIR = args[i + 1]; } ``` ### Technical Analysis Although `createBackup()` and `createIncrementalBackup()` accept a `destination` argument, both construct the actual backup path from the global `BACKUP_DIR`. The supplied destination is never used. The `--dir` option mutates `process.env.BACKUP_DIR` only after `WORKSPACE` and `BACKUP_DIR` have already been initialized. It therefore does not change the effective backup location in the current process. Users may consequently believe data is being copied to protected or isolated storage while it is actually retained under a workspace-derived directory. ### Attack Path 1. A user selects a restricted or encrypted destination for sensitive backup data. 2. The backup function ignores that destination. 3. The implementation writes the backup below the workspace-derived `backups/smart-backups` path. 4. Other users, processes, or workspace tooling with access to that directory may access the unexpected copy. ...[truncated 251 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse command-line arguments before calculating any effective directory. - Use the explicit `destination` parameter as the parent directory for each generated backup. - Clearly distinguish a workspace/data-directory override from a backup destination. - Canonicalize the effective destination and display it before processing files. - Fail closed when a requested destination cannot be created or differs from the effective destination. - Apply restrictive permissions to newly created backup directories and manifests where supported. - Add integration tests asserting that files are written only beneath the destination supplied by the caller. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
smart-backup.js:472
Finding
Incorrect Positional Argument Parsing Causes Unintended Filesystem Scope<![CDATA[ ## Vulnerability Details **File Location**: `smart-backup.js`, lines 472–528 **Vulnerability Type**: Incorrect command-line operand parsing **Risk Level**: Medium ### Vulnerable Code ```javascript switch (mode) { case 'backup': { const source = args[2]; const dest = args[3]; if (!source || !dest) { console.log('Usage: smart-backup.js --backup <source> <destination>'); } else { if (incremental) { createIncrementalBackup(source, dest, fromManifest, searchQuery === 'dryrun'); } else { createBackup(source, dest, searchQuery === 'dryrun'); } } break; } case 'sync': { const source = args[2]; const dest = args[3]; if (!source || !dest) { console.log('Usage: smart-backup.js --sync <source> <destination>'); } else { syncFiles(source, dest, searchQuery === 'dryrun', deleteFlag, forceFlag); } break; } case 'verify': { verifyBackup(args[2]); break; } case 'dedup': { const dups = findDuplicates(args[2] || WORKSPACE); console.log(`[smart-backup] Found ${dups.length} groups of duplicate files:\n`); for (const d of dups.slice(0, 5)) { console.log(` ${d.files.length} files (${formatBytes(d.size)}) — ${d.hash}`); for (const f of d.files) console.log(` → ${f.path}`); } break; } // ... case 'restore': { const backup = args[2]; const dest = args[3]; if (!backup || !dest) { console.log('Usage: smart-backup.js --restore <backup> <destination>'); } else { restoreBackup(backup, dest, forceFlag); } break; } } ``` ### Technical Analysis For a documented command such as `--dedup /chosen/path`, the option is at `args[0]` and its operand is at `args[1]`. The implementation instead reads `args[2]`. As a result: - `--dedup <dir>` ignores the supplied directory and falls back to `WORKSPACE`. - Normal backup, sync, and restore forms misassign or lose operands. - Verification receiv ...[truncated 943 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace manual positional indexing with a well-defined argument parser. - Associate operands with the selected mode independently of option ordering. - Reject unknown options, missing operands, duplicate modes, and surplus operands. - Never fall back to a broader directory when a user supplied an invalid or missing operand; terminate with a nonzero status instead. - Add tests for every documented invocation, including flags before and after operands. - Include explicit tests verifying that deduplication never reads outside the requested root. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
smart-backup.js:57
Finding
SHA-256 Integrity Digests Are Truncated to 64 Bits<![CDATA[ ## Vulnerability Details **File Location**: `smart-backup.js`, lines 57–62 **Vulnerability Type**: Insufficient hash strength for integrity verification **Risk Level**: Low ### Vulnerable Code ```javascript function getFileHash(filepath) { try { const content = fs.readFileSync(filepath); return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16); } catch { return null; } } ``` ### Technical Analysis The implementation computes SHA-256 but stores only the first 16 hexadecimal characters. This reduces the effective identifier to 64 bits rather than the documented 256-bit digest. These truncated values are used for backup verification, change detection, synchronization, and duplicate grouping. Collision resistance is therefore substantially weaker than full SHA-256, and unrelated files may be treated as identical if their prefixes collide. ### Attack Path 1. An attacker obtains or predicts the truncated hash stored in a manifest. 2. The attacker searches for alternate content with the same 64-bit SHA-256 prefix. 3. The attacker replaces a backed-up file with the colliding content. 4. Verification compares only the truncated value. 5. If the prefix matches, the modified content is accepted as valid. This path requires significant computational effort but is materially less resistant than attacking a full SHA-256 digest. ### Impact Assessment A successful collision can undermine integrity verification, change detection, synchronization decisions, or duplicate-file classification. It does not directly grant additional operating-system privileges. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Store and compare the complete 64-character hexadecimal SHA-256 digest. - Version the manifest format so truncated legacy hashes can be identified. - Regenerate full hashes for existing backups before treating them as verified. - Use constant-time comparison where hashes cross a meaningful trust boundary. - Update documentation and tests to assert a 256-bit digest length. ]]>
Vulnerability Patterns
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (8)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The documentation materially misstates behavior: it advertises compression that is not implemented, suggests user-specified destinations that may actually be ignored, and underdeclares persistent state plus destructive delete/overwrite modes. In backup/restore tooling, this mismatch is dangerous because users and agents may make safety decisions based on false assumptions, leading to data loss, privacy exposure, or writes outside intended locations.

Self-Modification

High
Category
Rogue Agent
Content
let incremental = false;
let fromManifest = null;
let deleteFlag = false;    // --delete: enable file deletion during sync
let forceFlag = false;      // --force: overwrite existing files (restore) or proceed with deletion (sync)

for (let i = 0; i < args.length; i++) {
  if (args[i] === '--backup') mode = 'backup';
Confidence
85% confidence
Finding
Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The markdown lists commands for backup, sync, and restore that act on user files and directories, but it does not warn that these operations can write data to destinations and potentially overwrite existing contents. For markdown files, safety-relevant behavior affecting user data should be disclosed so users understand the risk before invoking the skill.

Lp3

Medium
Category
MCP Least Privilege
Confidence
85% confidence
Finding
The skill declares no explicit tool scope or permissions while the analyzer detected environment-related capabilities. In an agent setting, missing scope declarations can cause the skill to be invoked with broader-than-expected authority, reducing operator visibility into what resources it may access.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The manifest description and module header describe 'compression' as part of the skill, but the implemented backup paths simply copy files with fs.copyFileSync and even note 'in production, would compress'. This is a direct mismatch between the advertised behavior and the actual code behavior.

Vague Triggers

Low
Confidence
78% confidence
Finding
The description lists very general capabilities like 'Backup, sync, verify integrity, dedup, and restore' without narrowing when this skill should be selected versus ordinary conversational mentions of backing up or restoring files. In a manifest-context file, this lack of trigger specificity can cause unintended invocation overlap with common file-management requests.

Vague Triggers

Low
Confidence
72% confidence
Finding
The section encourages adding the skill to heartbeat/maintenance workflows with recurring actions, but it does not clearly define when the skill should or should not run, or what safeguards determine applicability. For manifest/markdown guidance, this can make invocation scope too broad in automated maintenance contexts.

Intent-Code Divergence

Low
Confidence
94% confidence
Finding
The comment explicitly frames the following block as a placeholder for compression, but the code beneath it performs uncompressed file copies only. Because the skill documentation presents compression as a current capability, this comment highlights an intent-code divergence rather than a mere omitted detail.

Static analysis

No suspicious patterns detected.