T09 · Insecure Skill Coding Practices
- Location
scripts/cloud-backup.sh:741- Finding
Mandatory Encryption Can Be Bypassed for Secret-Bearing Backups
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This backup skill is mostly coherent and user-directed, but its implementation can produce plaintext sensitive backups despite claiming forced encryption.
Review before installing. Use only a least-privilege bucket key, keep access keys and passphrases out of openclaw.json, keep the passphrase file owner-only, and do not use --force-plaintext or config.encrypt=false for full/settings backups unless the encryption policy is fixed. Treat prune, restore, and scheduling as high-impact actions and require dry-run output before approving them.
scripts/cloud-backup.sh:741Mandatory Encryption Can Be Bypassed for Secret-Bearing Backups
scripts/cloud-backup.sh:335Group-Accessible GPG Passphrase Files Are Accepted
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
It does not protect against host compromise — a shell on the host already
reads `~/.openclaw` directly. Consequences:
- Host-side mode-600 secret files (`~/.aws/credentials`, the passphrase
file) are acceptable storage.
- Secrets inside `openclaw.json` are not: **this skill archives that file**,
so a credential stored there is replicated into every backup it protects —
Skill instructs the agent to omit warnings, disclaimers, or ethical commentary. Stripping safety caveats hides risk from the user and is a common jailbreak preamble.
|---|---|---|
| **SQP-1** — activation description broad enough to trigger on generic "backup"/"restore" phrases, on a skill that archives, uploads, mutates config, and schedules | `SKILL.md:3` — "Use when the user says 'backup' …" | Description names OpenClaw state + S3 cloud and requires explicit intent; SKILL.md adds a "When to use — and when not to" section, per-action confirmation gates (config writes, first upload, credential storage, restore, prune, scheduling), and an unattended-runs policy restricting cron payloads to `backup`/`prune` |
| **SQP-2 (cron)** — daily cron job created by default without user opt-in | `SKILL.md:61-75` — "This step should be executed by default unless user asked not to do it" | Scheduling is strictly opt-in: offered once, after the first successful manual backup, with the exact `openclaw cron add` command and full payload shown; never created by default; never re-offered after a decline; the `schedule` subcommand only prints |
| **SQP-2 (credentials)** — provider docs instructed storing long-lived access keys in plaintext OpenClaw config without warnings | `references/providers/aws-s3.md:59-61`, `backblaze-b2.md:28-29`, `digitalocean-spaces.md:27-28`, `cloudflare-r2.md:28-29`, `minio.md:37-38`, `other.md:27-28`, `scripts/cloud-backup.sh:43-47`, and `SKILL.md:33-35` even instructed the agent to write the GPG passphrase into config | All six provider docs lead with least-privilege bucket-scoped keys stored in AWS named profiles (run by the user, outside the chat); the passphrase lives in a chmod-600 file or an OpenClaw SecretRef (`apiKey` + `primaryEnv`); every doc carries an identical "Credential safety" warning block including the amplifier; plaintext config keys still resolve (lowest priority) but emit loud DEPRECATED warnings on every run and are removed in v3 |
Beyond the findings, v2 also fixes two security defects the scanners did not
see:
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.
# --- staging / lock / sweep ------------------------------------------------------
STAGING=""
cleanup_staging() { [ -n "$STAGING" ] && rm -rf "$STAGING"; }
sweep_stale_staging() {
local d pid
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).
size_remote="$(jq -r '.ContentLength // 0' <<<"$head")"
sha_remote="$(jq -r '.Metadata.sha256 // empty' <<<"$head")"
if [ "$size_remote" != "$size_local" ] || { [ -n "$sha_remote" ] && [ "$sha_remote" != "$sha" ]; }; then
s3 rm "s3://$BUCKET/$remote_key" >/dev/null 2>&1 || true
fail "$E_REMOTE_VERIFY" "remote object mismatch (size $size_remote vs $size_local)"
fi
fi
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).
local n=$(( ${#rk[@]} - KEEP )) i
info "Remote retention: pruning $n $1 archive(s) (keep $KEEP)"
for ((i = 0; i < n; i++)); do
s3 rm "s3://$BUCKET/${rk[$i]}" >/dev/null
s3 rm "s3://$BUCKET/${rk[$i]}.sha256" >/dev/null 2>&1 || true
done
fi
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).
info "Remote retention: pruning $n $1 archive(s) (keep $KEEP)"
for ((i = 0; i < n; i++)); do
s3 rm "s3://$BUCKET/${rk[$i]}" >/dev/null
s3 rm "s3://$BUCKET/${rk[$i]}.sha256" >/dev/null 2>&1 || true
done
fi
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).
for key in "${rk[@]}"; do
ts="$(arc_ts "$key")"; [ -n "$ts" ] || continue
if [ "$ts" -lt "$cutoff" ]; then
s3 rm "s3://$BUCKET/$key" >/dev/null
s3 rm "s3://$BUCKET/$key.sha256" >/dev/null 2>&1 || true
fi
done
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).
ts="$(arc_ts "$key")"; [ -n "$ts" ] || continue
if [ "$ts" -lt "$cutoff" ]; then
s3 rm "s3://$BUCKET/$key" >/dev/null
s3 rm "s3://$BUCKET/$key.sha256" >/dev/null 2>&1 || true
fi
done
fi
The skill invokes shell commands (bash ..., openclaw, aws, gpg) but does not declare any explicit tool scope such as permissions or allowed-tools. That creates an authorization gap where an agent/runtime may grant broader execution than intended, making accidental or unauthorized command execution easier in a highly sensitive backup/restore workflow.
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
| Secret | Home | How |
|---|---|---|
| S3 key pair | AWS named profile (`~/.aws/credentials`) | `aws configure --profile openclaw-backup && chmod 600 ~/.aws/credentials`, then set `config.profile` |
| GPG passphrase | passphrase file | `umask 077 && openssl rand -base64 32 > ~/.openclaw/credentials/cloud-backup.passphrase`, then set `config.passphraseFile` |
## Resolution order (what the script actually does)
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
| Secret | Home | How |
|---|---|---|
| S3 key pair | AWS named profile (`~/.aws/credentials`) | `aws configure --profile openclaw-backup && chmod 600 ~/.aws/credentials`, then set `config.profile` |
| GPG passphrase | passphrase file | `umask 077 && openssl rand -base64 32 > ~/.openclaw/credentials/cloud-backup.passphrase`, then set `config.passphraseFile` |
## Resolution order (what the script actually does)
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
| Secret | Home | How |
|---|---|---|
| S3 key pair | AWS named profile (`~/.aws/credentials`) | `aws configure --profile openclaw-backup && chmod 600 ~/.aws/credentials`, then set `config.profile` |
| GPG passphrase | passphrase file | `umask 077 && openssl rand -base64 32 > ~/.openclaw/credentials/cloud-backup.passphrase`, then set `config.passphraseFile` |
## Resolution order (what the script actually does)
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.
3. **OpenClaw secret ref** — `config.passphraseRef` (below). Works from any
shell; aborts with exit 14 if configured but unresolvable.
4. `config.passphraseFile` — path to a mode-600 file. **Recommended simple
default.** The script refuses world-readable files and warns on group
access. The passphrase is passed to gpg over a file descriptor — never on
a command line (v1 leaked it into `ps`/`/proc/*/cmdline`).
5. DEPRECATED: `skills.entries.cloud-backup.env.GPG_PASSPHRASE` plaintext in
No suspicious patterns detected.