Back to skill

Security audit

Openclaw Backup

Security checks for vulnerabilities and agentic risk

Overview

This backup skill is mostly purpose-aligned, but it can move broad workspace data and encrypted secret archives to GitHub under weaker controls than its documentation suggests.

Review and modify this skill before installing in a sensitive environment. Treat operational archives as private and potentially sensitive, do not push backups to GitHub unless you have inspected their contents, pass secrets to upload only by explicit intent, use strong age keys, and only restore archives from a trusted source.

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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/backup.sh:181
Finding
Unencrypted operational backups can expose sensitive workspace data to GitHub<![CDATA[ ## Vulnerability Details **File Location**: `scripts/backup.sh:181-190`, with the upload path in `scripts/push-to-github.sh:127-141` **Vulnerability Type**: Sensitive data exposure caused by overbroad backup scope and inadequate secret detection **Risk Level**: High ### Vulnerable Code ```bash copy_dir "$OPENCLAW_DIR/workspace" "$OP_STAGE/openclaw/workspace" "workspace/" if [ -f "$OPENCLAW_DIR/openclaw.json" ]; then mkdir -p "$OP_STAGE/openclaw" redact_openclaw_json "$OPENCLAW_DIR/openclaw.json" "$OP_STAGE/openclaw/openclaw.json" record_file "openclaw.json (redacted)" info "Added: openclaw.json (redacted)" else warn "Missing file, skipped: $OPENCLAW_DIR/openclaw.json" fi copy_file "$OPENCLAW_DIR/cron/jobs.json" "$OP_STAGE/openclaw/cron/jobs.json" "cron/jobs.json" ``` The resulting archive is copied into a Git repository and pushed: ```bash cp "$ARCHIVE_PATH" "$REPO_DIR/archives/$(basename "$ARCHIVE_PATH")" cp "$MANIFEST_PATH" "$REPO_DIR/archives/$(basename "$MANIFEST_PATH")" if [ -n "$SECRETS_PATH" ]; then cp "$SECRETS_PATH" "$REPO_DIR/archives/$(basename "$SECRETS_PATH")" fi ( cd "$REPO_DIR" git add .gitignore archives/* if git diff --cached --quiet; then info "No changes to commit." else git commit -m "Backup $(basename "$ARCHIVE_PATH")" >/dev/null git push origin HEAD >/dev/null info "Pushed backup to $REMOTE" fi ) ``` ### Technical Analysis The backup process recursively copies the entire OpenClaw workspace into an unencrypted operational archive. Only `openclaw.json` is passed through the key-name-based redaction routine. No exclusion or content-scanning logic is applied to the workspace. Consequently, the operational archive may contain: - `.env` files located below the workspace - Private keys or credential exports - API tokens embedded in scripts or configuration files - Personal information in `MEMORY.md` and daily memory files - Private Git repository metadata and remote URLs - Authentication dat ...[truncated 1914 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not classify the operational archive as cloud-safe by default. 2. Encrypt the complete backup archive before any cloud transfer, not only the known secrets tier. 3. Add deny-list exclusions for at least: - `.env` and `.env.*` - Private keys and certificates - Credential and token files - `.git/` directories - Package caches and `node_modules` - Configurable operator-defined sensitive paths 4. Add content scanning for common token, key, password, and private-key patterns before creating or uploading an operational archive. 5. Fail closed when potential secrets are detected and require explicit operator review. 6. Provide an allow-list mode that backs up only specifically documented operational files. 7. Display a file inventory and sensitive-data warning before an upload. 8. Add automated tests proving that workspace-resident `.env` files, private keys, and token fixtures are not included in cloud-bound archives. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/push-to-github.sh:90
Finding
GitHub uploader automatically includes encrypted secret archives without explicit upload consent<![CDATA[ ## Vulnerability Details **File Location**: `scripts/push-to-github.sh:90-104` and `scripts/push-to-github.sh:127-141` **Vulnerability Type**: Unintended transfer of sensitive authentication material **Risk Level**: High ### Vulnerable Code ```bash if [ -n "$SECRETS_PATH" ]; then [ -f "$SECRETS_PATH" ] || die "Secrets archive not found: $SECRETS_PATH" [[ "$SECRETS_PATH" == *.age ]] || die "Refusing to push unencrypted secrets archive: $SECRETS_PATH" else EXPECTED_SECRETS="$(python3 - "$MANIFEST_PATH" <<'PY' import json, os, sys manifest = json.load(open(sys.argv[1], encoding='utf-8')) name = manifest.get('archives', {}).get('secrets', {}).get('file') or '' base = os.path.dirname(sys.argv[1]) print(os.path.join(base, name) if name else '') PY )" if [ -n "$EXPECTED_SECRETS" ] && [ -f "$EXPECTED_SECRETS" ]; then SECRETS_PATH="$EXPECTED_SECRETS" [[ "$SECRETS_PATH" == *.age ]] || die "Refusing to push unencrypted secrets archive: $SECRETS_PATH" fi fi ``` The automatically selected archive is then pushed: ```bash cp "$ARCHIVE_PATH" "$REPO_DIR/archives/$(basename "$ARCHIVE_PATH")" cp "$MANIFEST_PATH" "$REPO_DIR/archives/$(basename "$MANIFEST_PATH")" if [ -n "$SECRETS_PATH" ]; then cp "$SECRETS_PATH" "$REPO_DIR/archives/$(basename "$SECRETS_PATH")" fi ( cd "$REPO_DIR" git add .gitignore archives/* if git diff --cached --quiet; then info "No changes to commit." else git commit -m "Backup $(basename "$ARCHIVE_PATH")" >/dev/null git push origin HEAD >/dev/null info "Pushed backup to $REMOTE" fi ) ``` ### Technical Analysis When the operator does not provide `--secrets`, the script reads the supplied manifest, locates an adjacent encrypted secrets archive, and sets `SECRETS_PATH` automatically. It then copies and commits that archive. This contradicts the documented security model: - The documented GitHub command is described as operational-only. - Secret archives are described as local-only by default. - The ...[truncated 1839 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove automatic secret archive discovery from the default upload path. 2. Upload secret archives only when the operator explicitly supplies `--secrets`. 3. Require a second intentional flag, such as `--allow-secret-upload`, before transferring any secrets archive. 4. Display the destination repository, archive path, size, and checksum before upload. 5. Require interactive confirmation unless an explicit non-interactive authorization flag is present. 6. Validate that the supplied file is an age payload rather than relying only on the `.age` suffix. 7. Do not automatically commit encrypted secrets to Git history; consider a dedicated encrypted object store with retention controls. 8. Correct the documentation so that actual upload behavior and secret-locality guarantees are consistent. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/backup.sh:204
Finding
Plaintext secret archive is written to persistent storage with inherited permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/backup.sh:204-220` **Vulnerability Type**: Plaintext sensitive data exposure and insecure file permissions **Risk Level**: Medium ### Vulnerable Code ```bash if [ "$INCLUDE_SECRETS" -eq 1 ]; then if [ ! -f "$SEC_STAGE/openclaw/.env" ] && [ ! -d "$SEC_STAGE/openclaw/agents" ]; then warn "Secrets backup requested but no secrets files were found; skipping secrets archive." INCLUDE_SECRETS=0 else ( cd "$SEC_STAGE" tar -czf "$SECRETS_ARCHIVE" openclaw ) if [ -n "$AGE_RECIPIENT" ]; then age -r "$AGE_RECIPIENT" -o "$SECRETS_ENCRYPTED" "$SECRETS_ARCHIVE" else AGE_PASSPHRASE="$(cat "$AGE_PASSPHRASE_FILE")" age -p -o "$SECRETS_ENCRYPTED" "$SECRETS_ARCHIVE" <<< "$AGE_PASSPHRASE" fi rm -f "$SECRETS_ARCHIVE" info "Created encrypted secrets archive: $SECRETS_ENCRYPTED" fi fi ``` The persistent run directory is created without explicitly setting restrictive permissions: ```bash mkdir -p "$RUN_DIR" ``` ### Technical Analysis The script first creates a plaintext compressed archive containing `$HOME/.openclaw/.env` and the agent authentication directory. This plaintext tarball is written directly into the persistent backup run directory before it is passed to `age`. The script does not set `umask 077`, does not create the run directory with mode `0700`, and does not explicitly set archive permissions to `0600`. Its accessibility therefore depends on the invoking process's inherited umask and the permissions of parent directories. Although the plaintext file is deleted after encryption, `rm -f` only removes the directory entry. It does not guarantee secure erasure from the underlying filesystem, snapshots, journal, backup software, or storage recovery mechanisms. If encryption fails after the tarball is created, shell termination may also leave the plaintext archive in the persistent run directory because the exit trap cleans only `TMP_DIR`. ## ...[truncated 992 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Set `umask 077` at the beginning of scripts that process backup data. 2. Create backup run directories with mode `0700`. 3. Stream the tar archive directly into `age` so no plaintext archive is created: ```bash ( cd "$SEC_STAGE" tar -czf - openclaw ) | age -r "$AGE_RECIPIENT" -o "$SECRETS_ENCRYPTED" ``` 4. For passphrase mode, use a secure age-supported non-interactive mechanism that does not expose the passphrase through command arguments or inherited environment state. 5. Explicitly set encrypted archive permissions to `0600`. 6. Add an exit trap that removes any plaintext intermediate if one remains necessary. 7. Perform encryption in a mode-`0700` temporary directory rather than the persistent backup directory. 8. Document that unlinking a plaintext intermediate does not provide secure media erasure. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/restore.sh:186
Finding
Restore executes code supplied by an unauthenticated backup archive<![CDATA[ ## Vulnerability Details **File Location**: `scripts/restore.sh:186-193`, with unauthenticated checksum validation in `scripts/verify.sh:58-99` **Vulnerability Type**: Arbitrary code execution through a tampered backup set **Risk Level**: High ### Vulnerable Code After installation of archive content, the restore process executes a script taken from the restored workspace: ```bash HEALTHCHECK_SCRIPT="$OPENCLAW_DIR/workspace/scripts/pre-restart-check.sh" if [ -f "$HEALTHCHECK_SCRIPT" ]; then if ! bash "$HEALTHCHECK_SCRIPT"; then rollback die "Health check failed after restore; rolled back." fi else info "Health check script not found; skipped: $HEALTHCHECK_SCRIPT" fi ``` The verification mechanism trusts checksums from the supplied, unsigned manifest: ```python manifest_path, archive_path, secrets_path = sys.argv[1:] manifest = json.loads(Path(manifest_path).read_text(encoding='utf-8')) errors = [] def sha256(path): h = hashlib.sha256() with open(path, 'rb') as f: for chunk in iter(lambda: f.read(1024 * 1024), b''): h.update(chunk) return h.hexdigest() operational = manifest.get('archives', {}).get('operational', {}) if operational.get('file') != os.path.basename(archive_path): errors.append(f"Operational archive filename mismatch: manifest={operational.get('file')} actual={os.path.basename(archive_path)}") if operational.get('sha256') != sha256(archive_path): errors.append('Operational archive checksum mismatch') if int(operational.get('size') or 0) != os.path.getsize(archive_path): errors.append('Operational archive size mismatch') ``` The archive is extracted without a preflight member-safety validation: ```bash tar -xzf "$ARCHIVE_PATH" -C "$RESTORE_ROOT" [ -d "$RESTORE_ROOT/openclaw" ] || die "Operational archive does not contain top-level openclaw/ directory" cp -R "$RESTORE_ROOT/openclaw/." "$STAGING_DIR/" ``` ### Technical Analysis SHA-256 validation establishes only that the ar ...[truncated 2570 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Digitally sign manifests with a trusted key that is stored separately from the backup destination. 2. Verify the signature before parsing the manifest or extracting any archive content. 3. Do not automatically execute scripts restored from the archive. 4. Move health-check logic into a trusted script bundled with this Skill. 5. If archive-provided checks are necessary, require explicit operator authorization and display the script path, cryptographic hash, and origin before execution. 6. Preflight every archive member before extraction and reject: - Absolute paths - Paths containing `..` - Device files and FIFOs - Unsafe symbolic links - Hard links targeting paths outside the extraction root 7. Extract with restrictive ownership and permission options, ensuring stored owners are not restored. 8. Treat backups obtained from GitHub or other remote storage as untrusted until signature verification succeeds. 9. Add adversarial tests using replaced manifests, traversal entries, unsafe links, and malicious health-check scripts. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (16)

Credential Access

High
Category
Privilege Escalation
Content
| Archive | Contents | Where it goes |
|---------|----------|---------------|
| **Operational** `openclaw-backup-*.tar.gz` | Workspace (SOUL, MEMORY, scripts, skills, memory), config (redacted), crons | Local + Google Drive + GitHub ✅ |
| **Secrets** `openclaw-secrets-*.tar.gz.age` | .env (API keys), agent auth profiles (OAuth tokens) | 🔒 Local only, encrypted with `age` |
| **Manifest** `manifest.json` | Checksums, versions, file list, timestamps | Alongside both archives |

**Secrets never touch cloud storage unencrypted.** The push-to-github script hard-refuses if it detects unencrypted secrets.
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The supplied code is specifically a backup creation script. It stages selected OpenClaw files, creates a tar.gz operational archive, optionally creates and age-encrypts a secrets archive, and generates a manifest containing SHA-256 checksums and metadata. This aligns with part of the declared description: two-tier backup archives, age encryption for secrets, and manifest checksum generation. However, the description presents a broader skill covering backup and restore, atomic restore safety mechanisms, GitHub push behavior, and daily scheduling. None of those additional capabilities appear in this code chunk. Because the declared purpose materially overstates what this code actually does, the description does not accurately represent the supplied code chunk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The code chunk partially matches the declared description only in the narrow area of pushing backup artifacts to GitHub with a safeguard against uploading unencrypted secrets archives. However, the declared purpose describes a substantially broader backup-and-restore system with encryption, checksum verification, atomic restore protections, and scheduled backups. None of those core capabilities are implemented in this provided code. Since the actual chunk’s primary behavior is just GitHub repository management and pushing existing files, the description does not accurately represent what this code chunk itself does.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared description presents a broad backup/restore system with encryption, verification, restore safety, GitHub integration, and scheduling. The actual supplied code chunk is much narrower: it configures a scheduled cron job in OpenClaw that will later run backup.sh. While daily scheduled backups are mentioned in the description, this code does not itself implement backup creation, restore, encryption, checksum verification, rollback, or GitHub push. Its primary behavior is cron management, which is only one supporting aspect of the declared system. Because the provided chunk materially differs from the declared purpose and lacks most of the claimed capabilities, this should be flagged as a mismatch.

Credential Access

High
Category
Privilege Escalation
Content
copy_file "$OPENCLAW_DIR/cron/jobs.json" "$OP_STAGE/openclaw/cron/jobs.json" "cron/jobs.json"

if [ "$INCLUDE_SECRETS" -eq 1 ]; then
  copy_file "$OPENCLAW_DIR/.env" "$SEC_STAGE/openclaw/.env" ".env"
  copy_dir "$OPENCLAW_DIR/agents" "$SEC_STAGE/openclaw/agents" "agents/"
fi
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
copy_file "$OPENCLAW_DIR/cron/jobs.json" "$OP_STAGE/openclaw/cron/jobs.json" "cron/jobs.json"

if [ "$INCLUDE_SECRETS" -eq 1 ]; then
  copy_file "$OPENCLAW_DIR/.env" "$SEC_STAGE/openclaw/.env" ".env"
  copy_dir "$OPENCLAW_DIR/agents" "$SEC_STAGE/openclaw/agents" "agents/"
fi
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
TMP_OUTPUT="$(mktemp "${TMPDIR:-/tmp}/openclaw-snapshot.XXXXXX")"
STAGE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-snapshot-stage.XXXXXX")"
trap 'rm -f "$TMP_OUTPUT"; rm -rf "$STAGE_ROOT"' EXIT

bash "$BACKUP_SCRIPT" --no-secrets --output-dir "$STAGE_ROOT" >"$TMP_OUTPUT"
Confidence
100% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Chaining Abuse

High
Category
Tool Misuse
Content
TMP_OUTPUT="$(mktemp "${TMPDIR:-/tmp}/openclaw-snapshot.XXXXXX")"
STAGE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-snapshot-stage.XXXXXX")"
trap 'rm -f "$TMP_OUTPUT"; rm -rf "$STAGE_ROOT"' EXIT

bash "$BACKUP_SCRIPT" --no-secrets --output-dir "$STAGE_ROOT" >"$TMP_OUTPUT"
Confidence
75% confidence
Finding
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Session Persistence

Medium
Category
Rogue Agent
Content
### Weekly verify — Sundays at 3 AM
```bash
openclaw cron create \
  --name "OpenClaw Weekly Backup Verify" \
  --cron "0 3 * * 0" \
  --system-event "Run bash '$HOME/.openclaw/workspace/skills/openclaw-backup/scripts/weekly-verify.sh' and return the output exactly."
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.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill invokes shell scripts for backup, restore, scheduling, and GitHub push, but the manifest does not declare any tool restrictions such as permissions or allowed-tools. That omission widens the trust boundary: a consumer may authorize the skill without realizing it needs shell, file read, and file write capabilities, increasing the risk of unintended filesystem changes or command execution if the referenced scripts are unsafe or replaced.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The guide instructs users to run restore and restart operations that modify a live OpenClaw installation, but it does not prominently warn that these actions can overwrite the current state, interrupt service, and trigger rollback-sensitive changes. In a disaster-recovery skill this is expected functionality, but the missing caution increases the chance of operator error and unintended disruption.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The partial-restore examples copy files directly into live OpenClaw paths using cp, which can overwrite existing workspace, config, or cron data without any confirmation or backup step. Although intended for recovery, these commands are risky because they make targeted destructive changes outside the atomic restore workflow and could leave the agent in an inconsistent state.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The script silently removes snapshot run directories beyond the five most recent backups using shutil.rmtree. Although the script prints the final snapshot path, it does not disclose this retention-based deletion behavior to the user via comments, logs, or confirmation, making a destructive operation easy to miss.

Missing User Warnings

Medium
Confidence
82% confidence
Finding
This shell script deletes an existing cron job with the same name and creates a new one, which is a system-modifying operation. Although it prints status messages, those messages do not clearly warn the user beforehand that an existing scheduled task will be removed and replaced, and there is no confirmation prompt or explanatory comment/docstring disclosing that impact.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script prunes old backup run directories with `rm -rf` and only reports aggregate cleanup counts after completion. There is no confirmation prompt or explicit user-facing notice before deletion, so users may not realize the script performs irreversible cleanup when it runs.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The script removes `manifest.json` files lacking archives and deletes orphan secret archives with `rm -f`, but it provides no per-action warning, confirmation, or inline explanation near the destructive operations. Although a summary is printed afterward, that does not disclose the deletions before they occur.

Static analysis

No suspicious patterns detected.