Back to skill

Security audit

Openclaw Migrate

Security checks for vulnerabilities and agentic risk

Overview

This migration skill is coherent, but it copies secrets and scheduled jobs to another host with broad defaults and unsafe shell-command handling, so it needs careful review before installation.

Install only if you fully trust the destination host and are prepared for it to receive copied OpenClaw memory, configuration, API tokens, and scheduled jobs. Review and rotate sensitive tokens where appropriate, avoid hostile or untrusted SSH inputs, and prefer a version that uses safe argument handling, opt-in secret migration, and OpenClaw-specific cron migration instead of replacing the whole crontab.

Vulnerability Patterns
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • 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
Findings (4)

T06 · System Persistence

Error
Location
main.js:254
Finding
Automatic Remote Crontab Replacement Creates Persistent Execution<![CDATA[ ## Vulnerability Details **File Location**: `main.js:254-263` **Vulnerability Type**: Scheduled-task persistence and destructive crontab replacement **Risk Level**: Critical ### Vulnerable Code ```js // Sync cron jobs async function syncCron(host, user, key) { log('Syncing cron jobs...', 'cyan'); try { const result = await execCmd('crontab -l 2>/dev/null || echo ""'); const crons = result.stdout; if (crons.trim()) { // Save to remote const cmd = `ssh ${user}@${host} "echo '${crons.replace(/'/g, "'\\''")}' | crontab -"`; await execCmd(cmd); log('Cron jobs synced', 'green'); } else { log('No cron jobs to sync', 'gray'); } ``` ### Technical Analysis The migration reads the invoking user's entire local crontab and pipes it to `crontab -` on the target. This is a persistence mechanism because every transferred entry can continue executing after the migration process terminates and across future login sessions or reboots. The operation is performed automatically after the general migration confirmation. Users are not shown the cron entries, asked for separate consent, or allowed to select only OpenClaw-related jobs. In addition, `crontab -` replaces the target user's complete existing crontab rather than safely merging OpenClaw-specific entries. The cron content is also embedded in a shell command. Replacing single quotes alone is not sufficient protection in the surrounding nested shell context. Shell substitutions and other metacharacters in cron data may be interpreted while constructing or executing the SSH command. Although cron migration is disclosed in `README.md` and `SKILL.md`, transferring every user job exceeds the minimum scope necessary to migrate OpenClaw. Only explicitly identified OpenClaw jobs should be considered. ### Attack Path 1. An attacker, compromised application, or untrusted installer places a malicious entry in the source user's crontab. 2. The user starts the ...[truncated 970 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Make cron migration disabled by default and require a separate, explicit confirmation. - Display every proposed cron entry before installation. - Transfer only entries positively identified as belonging to OpenClaw. - Back up the target crontab before making any changes. - Merge approved entries with the existing target crontab instead of replacing it. - Transfer cron content over standard input using `spawn()` or `execFile()` with fixed argument arrays rather than interpolating it into a shell command. - Validate cron syntax and reject command substitutions, unexpected executables, unsafe paths, and unrelated jobs. - Provide a rollback procedure and report exactly which entries were installed. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
main.js:81
Finding
Shell Command Injection Through User-Controlled SSH Parameters<![CDATA[ ## Vulnerability Details **File Location**: `main.js:81-85`, `main.js:204-214`, and `main.js:316-318` **Vulnerability Type**: OS command injection **Risk Level**: Critical ### Vulnerable Code ```js // Execute remote command via SSH function sshExec(host, user, cmd, options = {}) { return new Promise((resolve, reject) => { const sshCmd = `ssh ${options.key ? `-i ${options.key}` : ''} ${user}@${host} "${cmd}"`; exec(sshCmd, { timeout: options.timeout || 60000 }, (error, stdout, stderr) => { ``` ```js // Ensure remote directory exists const remoteDir = path.dirname(file.remote).replace('~', '/home/' + user); await sshExec(host, user, `mkdir -p "${remoteDir}"`, { key, ignoreError: true }); // SCP file const scpCmd = `scp ${key ? `-i ${key}` : ''} -r "${file.local}" ${user}@${host}:"${file.remote}"`; await execCmd(scpCmd, { timeout: 60000 }); ``` ```js // Get target host info config.host = await prompt('New host IP/hostname: '); config.user = await prompt('SSH user (default: crix): ') || 'crix'; config.key = await prompt('SSH key path (optional, press Enter for default): ') || ''; ``` ### Technical Analysis The target hostname, SSH username, and key path originate from interactive input and are saved to `config.json`. These values are subsequently concatenated into command strings passed to `child_process.exec()`. `exec()` invokes a shell, so metacharacters such as command separators, command substitutions, redirects, and quoting characters are interpreted as shell syntax. No strict validation or reliable shell escaping is applied to `host`, `user`, or `key`. The same unsafe construction is used for SSH and SCP. Consequently, merely testing a configured connection can trigger a malicious value; a full migration is not required. The presence of an SSH key path in `README.md` is not itself unsafe, and the implementation does not write to the private key. The vulnerability is that the path is treated as executable shell text instead of an ...[truncated 1003 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace shell-based `exec()` calls with `spawn()` or `execFile()` and pass each SSH/SCP option as a separate argument. - Do not attempt to solve this solely with ad hoc escaping. - Validate host values as IP addresses or conservative DNS names. - Restrict usernames to the target platform's permitted username character set. - Resolve the key path with filesystem APIs and reject options, control characters, null bytes, and unexpected file types. - Insert `--` where supported to terminate command options. - Protect `config.json` with restrictive permissions, validate it again when loaded, and reject unknown properties. - Apply equivalent argument-safe handling to all SSH, SCP, cron, and profile-update operations. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
main.js:228
Finding
API Tokens Are Unsafely Embedded in Shell Commands and Plaintext Profile Files<![CDATA[ ## Vulnerability Details **File Location**: `main.js:37-42`, `main.js:187-193`, and `main.js:228-245` **Vulnerability Type**: Secret exposure and shell command injection **Risk Level**: High ### Vulnerable Code ```js const ENV_VARS_TO_SYNC = [ 'HA_URL', 'HA_TOKEN', 'GITHUB_TOKEN', 'BRAVE_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_SERVICE_ACCOUNT', ]; ``` ```js function getEnvVarsToSync() { const vars = {}; for (const v of ENV_VARS_TO_SYNC) { if (process.env[v]) { vars[v] = process.env[v]; } } return vars; } ``` ```js async function syncEnvVars(host, user, key, vars) { if (Object.keys(vars).length === 0) { log('No environment variables to sync', 'gray'); return true; } log('Syncing environment variables...', 'cyan'); // Add to remote shell profile const envLines = Object.entries(vars).map(([k, v]) => `export ${k}="${v}"`).join('\n'); const profileLine = `\n# OpenClaw Migrated ENV\n${envLines}\n`; for (const profile of ['.bashrc', '.profile', '.zshrc']) { try { const checkCmd = `ssh ${user}@${host} "grep -q '# OpenClaw Migrated' ~/.${profile} 2>/dev/null || echo '${profileLine}' >> ~/.${profile}"`; await execCmd(checkCmd); } catch (e) {} } ``` ### Technical Analysis Sensitive values, including Home Assistant, GitHub, Brave, and Google credentials, are read from the process environment. They are inserted directly into generated shell source and then embedded in an SSH command passed to `exec()`. The values are not encoded or escaped for either of the nested shell contexts. A value containing command substitution, quotes, backticks, newlines, or other shell syntax can alter local or remote command execution. It is not sufficient that the generated export statement uses double quotes because that still permits command substitution when the profile is sourced. The generated command can expose secrets to process inspection and diagnostic tooling. The credentials are also writt ...[truncated 1680 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not migrate secrets by default; require explicit confirmation for each credential. - Prefer a supported secret manager, operating-system credential store, or OpenClaw-specific protected credential mechanism. - Never embed credentials in a shell command. - Transfer structured secret data through a protected stream or SFTP operation without shell interpretation. - If a protected file is unavoidable, create it atomically with mode `0600` and ensure its containing directory is inaccessible to other users. - Avoid storing long-lived secrets in general shell startup files. - Validate environment variable names against a fixed allowlist and serialize values with a format that is not interpreted as shell source. - Correct profile paths by using `['.bashrc', '.profile', '.zshrc']` directly rather than adding another dot. - Rotate any credentials that may already have been migrated through this implementation. ]]>

T08 · Insecure Dependencies

Warning
Location
main.js:276
Finding
Unpinned Global Installation of OpenClaw Introduces Supply-Chain Risk<![CDATA[ ## Vulnerability Details **File Location**: `main.js:276-285` **Vulnerability Type**: Unpinned third-party package installation **Risk Level**: Medium ### Vulnerable Code ```js async function installOpenClaw(host, user, key) { log('Installing OpenClaw on remote host...', 'cyan'); try { // Check if npm is available await sshExec(host, user, 'which npm', { key }); // Install OpenClaw const installCmd = 'npm install -g openclaw'; await sshExec(host, user, installCmd, { key, timeout: 120000 }); ``` ### Technical Analysis The migration installs the latest package published under the `openclaw` name rather than a reviewed and pinned version. No integrity hash, lockfile, trusted registry configuration, package signature, or provenance verification is used. NPM installation may execute package lifecycle scripts. Therefore, compromise of the package, maintainer account, registry resolution, or an unexpectedly malicious future release can turn the migration into remote code execution. The global installation flag increases the installation scope within the target account or environment. The actual privilege level depends on the target's NPM configuration and whether global installation is permitted without elevation. ### Attack Path 1. The expected `openclaw` package or its dependency chain is compromised, or an unsafe registry configuration resolves the name to an attacker-controlled package. 2. The migration determines that OpenClaw is absent and the user approves installation. 3. The target executes `npm install -g openclaw` without selecting a reviewed version. 4. NPM downloads the current resolved package and dependencies. 5. Malicious package code or lifecycle scripts execute on the target. 6. The payload obtains the privileges of the SSH user and may persist through the globally installed package or subsequent gateway execution. ### Impact Assessment A compromised package can execute arbitrary commands as the ta ...[truncated 300 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin OpenClaw to a reviewed exact version rather than installing the latest release. - Verify package integrity, provenance, and registry origin before installation. - Use a controlled registry and reject configuration that redirects package resolution to an untrusted source. - Prefer a lockfile or a signed, reproducible deployment artifact. - Avoid global installation where a user-scoped or isolated installation is sufficient. - Display the exact version, registry, and expected permissions before asking for installation consent. - Consider disabling lifecycle scripts during verification and only enabling required scripts after package review. ]]>
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
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (11)

Credential Access

High
Category
Privilege Escalation
Content
$ openclaw-migrate setup
New host IP/hostname: 192.168.1.50
SSH user: crix
SSH key path: ~/.ssh/id_rsa
```

## Usage
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The tool explicitly collects and migrates multiple sensitive environment variables, including unrelated third-party secrets such as GITHUB_TOKEN, BRAVE_API_KEY, and GOOGLE credentials, to another host. For a migration utility, bulk secret exfiltration to a remote machine materially increases exposure, especially because these secrets may grant access beyond OpenClaw itself and are transferred without scope limitation or per-secret confirmation.

Missing User Warnings

High
Confidence
99% confidence
Finding
Sensitive environment variables are selected for transfer and later written to remote shell startup files without a dedicated warning or consent step. Because these values include tokens and service account material, silent migration can cause users to disclose credentials to a host they did not intend to fully trust.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The code copies the user's entire local crontab to the remote host, which is outside the narrow scope of migrating OpenClaw. This can recreate unrelated scheduled tasks, propagate persistence mechanisms, overwrite the destination crontab, and unintentionally execute privileged or security-sensitive jobs on another system.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill is presented as a one-command SSH migration that auto-installs software, syncs configuration, and copies cron jobs to another machine. These actions can materially change the target system and scheduled tasks, but the documentation does not warn users about system integrity impact or recommend reviewing what will be modified before execution.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The markdown explicitly states that environment variables such as `HA_TOKEN`, `GITHUB_TOKEN`, and API keys will be synced to another host. This is a privacy- and security-sensitive behavior, but the README does not include a warning about credential transfer, trust requirements for the target host, or the risk of copying secrets to a new system.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill explicitly states it migrates OpenClaw config, skills, memory, and tokens to another host, but it does not prominently warn users that this operation transfers sensitive credentials and private agent state over SSH to a potentially different trust boundary. Users could run the migration without appreciating that API tokens, Home Assistant secrets, and other sensitive data will be replicated onto the destination system, increasing the risk of credential exposure or compromise if the target host is untrusted or misconfigured.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The migration flow and system data sections indicate that cron jobs and gateway configuration are copied and that the gateway is started on the new host, yet the documentation does not clearly warn that this may activate automated tasks and network-facing services on the destination. This can unexpectedly duplicate scheduled execution, expose services, or cause conflicting automation behavior if the old and new hosts run concurrently.

File System Enumeration

Medium
Category
Data Exfiltration
Content
// Check openclaw workspace
  const workspacePath = path.join(process.env.HOME || '/home/crix', '.openclaw');
  if (fs.existsSync(workspacePath)) {
    // Walk directory
    const walk = (dir, prefix = '') => {
      const items = fs.readdirSync(dir);
      for (const item of items) {
Confidence
60% confidence
Finding
Code scans file system directories looking for sensitive files. This could be reconnaissance for credential theft.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The cron synchronization step occurs automatically once migration is confirmed, but there is no explicit warning that scheduled tasks will be created or overwrite the remote crontab. This can surprise users and cause unintended execution of commands on the new host, including jobs unrelated to OpenClaw.

Session Persistence

Medium
Category
Rogue Agent
Content
log('Syncing cron jobs...', 'cyan');
  
  try {
    const result = await execCmd('crontab -l 2>/dev/null || echo ""');
    const crons = result.stdout;
    
    if (crons.trim()) {
Confidence
90% confidence
Finding
Reading the local crontab and reinstalling it on a remote host creates persistence on the destination system and can carry over automated execution mechanisms unrelated to this application. In the context of an agent skill, this is more dangerous because it extends behavior across hosts and may perpetuate scheduled execution without focused user understanding.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
main.js:70