Back to skill

Security audit

Machine Config Migrator

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed machine-configuration migration skill, but applying bundles can overwrite local files and optionally execute plugin installers, so it should be reviewed before use.

Use this only with bundles you created and trust. Run --dry-run first, prefer --plugin-mode suggest or none, review the manifest and file list before applying, and avoid sharing generated bundles because shell, Git, editor, Alfred, and SSH config can contain sensitive hostnames, tokens, or workflow data.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/apply_config_bundle.py:27
Finding
Archive extraction permits path traversal and unsafe link handling<![CDATA[ ## Vulnerability Details **File Location**: `scripts/apply_config_bundle.py:27-33` **Vulnerability Type**: Unsafe archive extraction **Risk Level**: High ### Vulnerable Code ```python def safe_extract_tar(archive: tarfile.TarFile, destination: Path) -> None: destination = destination.resolve() for member in archive.getmembers(): member_path = (destination / member.name).resolve() if not str(member_path).startswith(str(destination)): raise ValueError(f"Unsafe tar member path: {member.name}") archive.extractall(path=destination) ``` ### Technical Analysis The extraction guard uses a string-prefix comparison to determine whether a member remains under the extraction directory. String prefixes do not represent filesystem ancestry. For example, if the extraction directory is `/tmp/bundle`, a resolved path such as `/tmp/bundle-evil/file` still begins with the string `/tmp/bundle`. The implementation also does not reject or validate symbolic links and hard links. A crafted archive can contain a link whose target points outside the extraction directory and subsequent members that write through that link. Calling `archive.extractall()` after only validating member names does not reliably contain those link-based writes. The bundle is therefore treated as trusted filesystem input even though it can originate from another machine or an untrusted transfer channel. ### Attack Path 1. An attacker creates a tar archive containing traversal paths or archive links that resolve outside the intended temporary extraction directory. 2. The attacker supplies the archive as a migration bundle. 3. The user runs `apply_config_bundle.py --bundle <malicious-archive>`. 4. The flawed string-prefix check accepts a path that is not actually a descendant of the extraction directory, or fails to account for a malicious link target. 5. `archive.extractall()` writes an archive member outside the temporary directory. 6. Files accessible t ...[truncated 538 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Reject absolute archive member names and any member containing a `..` path component. - Replace string-prefix checks with filesystem-aware containment checks such as `resolved_path.is_relative_to(destination)`. - Reject symbolic links and hard links unless they are explicitly required. - If links must be supported, resolve and validate each link target against the extraction root before extraction. - On supported Python versions, use `tarfile` extraction filters designed to reject dangerous metadata and paths. - Extract members individually only after validation rather than passing the complete archive directly to `extractall()`. - Add regression tests for absolute paths, `../` traversal, sibling-prefix paths, symlink traversal, and hard-link traversal. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/apply_config_bundle.py:232
Finding
Manifest-controlled paths can escape target and backup directories<![CDATA[ ## Vulnerability Details **File Location**: `scripts/apply_config_bundle.py:232-263` **Vulnerability Type**: Arbitrary file read, backup, deletion, and overwrite through path traversal **Risk Level**: High ### Vulnerable Code ```python for component in selected: details = included.get(component, {}) if not isinstance(details, dict): continue rel_paths = details.get("paths", []) if not isinstance(rel_paths, list): continue for rel_path_obj in rel_paths: if not isinstance(rel_path_obj, str): continue src = payload_home / rel_path_obj dst = target_home / rel_path_obj if not src.exists() and not src.is_symlink(): print(f"[warn] missing in bundle: {rel_path_obj}") continue print(f"[apply] {component}: {rel_path_obj}") if args.dry_run: continue if dst.exists() or dst.is_symlink(): backup_path = backup_dir / rel_path_obj backup_existing(dst, backup_path) copy_path(src, dst) writes += 1 if args.verbose: print(f"[ok] wrote {dst}") ``` ### Technical Analysis The `paths` values in `manifest.json` are used directly to construct source, destination, and backup paths. The code does not reject: - Absolute paths - Parent-directory components such as `../` - Paths outside the supported path list for a component - Resolved paths that escape `payload_home`, `target_home`, or `backup_dir` With `pathlib`, joining a base path with an absolute path discards the base path. Relative traversal components can likewise escape the intended directory after resolution. The destination is eventually passed to `copy_path()`, which removes an existing destination before copying. C ...[truncated 1369 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Define an immutable allowlist of supported relative paths for every component and reject manifest entries outside that allowlist. - Reject empty paths, absolute paths, drive-qualified paths, null bytes, and any `..` component. - Resolve every source, destination, and backup path before access. - Enforce containment with checks equivalent to: - `src.resolve().is_relative_to(payload_home.resolve())` - `dst.resolve().is_relative_to(target_home.resolve())` - `backup_path.resolve().is_relative_to(backup_dir.resolve())` - Account for symlinked parent directories by validating resolved parent paths immediately before each write. - Validate the complete manifest against a strict schema before performing any filesystem operation. - Refuse unknown component names instead of treating arbitrary manifest keys as valid components. - Add tests covering absolute paths, traversal paths, symlinked parents, malformed manifests, and unsupported component/path combinations. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/apply_config_bundle.py:273
Finding
Plugin mode executes code and configuration restored from an untrusted bundle<![CDATA[ ## Vulnerability Details **File Location**: `scripts/apply_config_bundle.py:83-106, 273-281` **Vulnerability Type**: Arbitrary code execution through restored scripts and editor configuration **Risk Level**: High ### Vulnerable Code ```python if "tmux" in selected: tpm_path = target_home / ".tmux/plugins/tpm" if tpm_path.exists(): commands.append("~/.tmux/plugins/tpm/bin/install_plugins") else: commands.append( "git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm " "&& ~/.tmux/plugins/tpm/bin/install_plugins" ) if "vim" in selected: init_vim = target_home / ".config/nvim/init.vim" init_lua = target_home / ".config/nvim/init.lua" vimrc = target_home / ".vimrc" if any(path.exists() and "plug#begin" in path.read_text(errors="ignore") for path in [vimrc, init_vim]): commands.append("vim +PlugInstall +qall || true") if init_lua.exists(): text = init_lua.read_text(errors="ignore") if "lazy.nvim" in text: commands.append("nvim --headless '+Lazy! sync' +qa || true") if "packer" in text: commands.append("nvim --headless '+PackerSync' +qa || true") ``` ```python if args.plugin_mode == "run" and not args.dry_run: print("Running plugin commands...") for command in plugin_commands: result = subprocess.run( command, shell=True, cwd=target_home, check=False, ) if result.returncode != 0: print(f"[warn] command failed ({result.returncode}): {command}") ``` ### Technical Analysis When `--plugin-mode run` is selected, the script executes commands after restoring files from the bundle. Several execution paths consume attacker-controlled migrated content: - ...[truncated 1821 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Treat every imported bundle and restored configuration file as untrusted. - Do not execute scripts directly from the restored payload. - Separate restoration from plugin execution and require an explicit warning and confirmation that identifies every executable file or configuration that will be loaded. - Authenticate bundles with a trusted digital signature and verify the signature before offering execution. - Invoke subprocesses with argument arrays and `shell=False`. - Use validated absolute paths derived from `target_home`; do not use `~` in execution commands. - Where supported, launch editors with user configuration disabled and invoke a trusted installer entry point rather than loading migrated configuration. - Run plugin installation in a sandbox or restricted subprocess without sensitive environment variables or unnecessary filesystem access. - Consider limiting automatic mode to printing commands and requiring users to review and run them manually. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/apply_config_bundle.py:87
Finding
Plugin installers retrieve mutable, unpinned remote dependencies<![CDATA[ ## Vulnerability Details **File Location**: `scripts/apply_config_bundle.py:87-90, 122-126` **Vulnerability Type**: Unpinned third-party dependency retrieval **Risk Level**: Medium ### Vulnerable Code ```python else: commands.append( "git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm " "&& ~/.tmux/plugins/tpm/bin/install_plugins" ) ``` ```python known_map = { "zsh-autosuggestions": "git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions", "zsh-syntax-highlighting": "git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting", "zsh-completions": "git clone https://github.com/zsh-users/zsh-completions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-completions", } ``` ### Technical Analysis The generated commands clone mutable default branches from remote Git repositories without pinning a commit, validating a signed tag, or checking an expected digest. The code reviewed during the Skill audit is therefore not necessarily the code later retrieved by a user. The TPM repository is immediately followed by execution of its installer. Zsh plugins are not immediately executed by this function, but their source code is subsequently loaded by the user's shell configuration. HTTPS protects transport against ordinary network modification but does not protect against compromised repositories, compromised maintainer accounts, malicious upstream changes, or unexpected changes to mutable branches. ### Attack Path 1. A referenced upstream repository or maintainer account is compromised, or the default branch receives malicious content. 2. A user runs the migration with `--plugin-mode run`. 3. The script clones the current default branch without verifying a known revision. 4. For TPM, the newly ...[truncated 641 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin each repository to an audited full commit hash or a verified signed release tag. - Fetch the repository and explicitly check out the approved revision before running or loading any code. - Verify release signatures or compare downloaded content against maintained cryptographic hashes. - Record the repository URL and exact approved revision in a version-controlled dependency manifest. - Display the exact revision to the user before installation. - Avoid immediately executing newly cloned code; provide a review step between download and execution. - Periodically review and deliberately update pinned revisions rather than following mutable default branches automatically. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (13)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
if args.plugin_mode == "run" and not args.dry_run:
            print("Running plugin commands...")
            for command in plugin_commands:
                result = subprocess.run(
                    command,
                    shell=True,
                    cwd=target_home,
Confidence
98% confidence
Finding
Using shell=True is a classic command-execution hazard because the shell parses metacharacters, expansions, and command chaining. Here it is especially risky because the tool operates on imported configuration/manifest data and runs in the user's home directory, so any future expansion of manifest-controlled values or environment manipulation could lead to unintended command execution on the destination machine.

Credential Access

High
Category
Privilege Escalation
Content
],
    "ssh": [
        ".ssh/config",
        ".ssh/known_hosts",
    ],
}
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill instructs use of local scripts that read files, write configuration, and execute shell-level actions, but it declares no explicit tool scope or permission boundaries. In an agent environment, this can allow the skill to operate with broader-than-expected capabilities, increasing the chance of unintended file modification, unsafe command execution, or access to sensitive user data during migration.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The plugin-command builder does more than provide guidance: it prepares commands for git clone, vim/neovim sync, and Emacs package installation, enabling live modification of the target environment and network retrieval. For a migration/apply tool, this broadens behavior beyond justified file restoration and creates a pathway for unintended package installation based on imported configuration state.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
This skill goes beyond copying configuration files and can actively execute shell commands to install or sync plugins. In the context of applying a config bundle from another machine, the bundle should be treated as untrusted input; coupling restore operations with live command execution increases the blast radius from file overwrite to code execution and network access.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if args.plugin_mode == "run" and not args.dry_run:
            print("Running plugin commands...")
            for command in plugin_commands:
                result = subprocess.run(
                    command,
                    shell=True,
                    cwd=target_home,
Confidence
97% confidence
Finding
The code executes plugin commands with subprocess.run(..., shell=True), which turns command strings into shell-interpreted input. While the current commands are mostly hardcoded, some are assembled from manifest-derived plugin names and environment-variable expansions, and the feature performs network-fetching/install actions on the target machine; this creates unnecessary command-execution risk in a migration tool handling potentially untrusted bundles.

Session Persistence

Medium
Category
Rogue Agent
Content
import argparse
import json
import plistlib
import re
import shutil
import tarfile
Confidence
75% 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
import argparse
import json
import plistlib
import re
import shutil
import tarfile
Confidence
75% 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
import argparse
import json
import plistlib
import re
import shutil
import tarfile
Confidence
75% 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
import argparse
import json
import plistlib
import re
import shutil
import tarfile
Confidence
75% 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
import argparse
import json
import plistlib
import re
import shutil
import tarfile
Confidence
75% 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.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
This copy routine packages selected configuration files verbatim into a tar.gz archive, and those files can contain secrets such as SSH host aliases, tokens in git or shell configs, API keys in editor settings, and private workflow data. The script does not present an explicit warning, preview, or confirmation about sensitive contents, which increases the chance of accidental secret disclosure when users share or store the bundle.

Context-Inappropriate Capability

Low
Confidence
90% confidence
Finding
The manifest stores the full absolute source home path, which is not needed to restore dotfiles and can reveal the original username and host-specific directory structure to anyone who receives the bundle. In this migration context the bundle is meant to be portable, so retaining extra host-identifying metadata unnecessarily expands the exposure surface.