Back to skill

Security audit

Credential Vault

Security checks for vulnerabilities and agentic risk

Overview

This is a local credential vault with no phone-home behavior, but its security design and documented workflows can expose stored secrets or run unintended shell commands.

Install only after reviewing the tradeoffs. Use it as a local development convenience, not for production or shared machines. Do not use the documented eval workflows with untrusted or imported secret values, avoid printing secrets to terminals, lock the vault before logout or reboot, and treat the session file as equivalent to all stored credentials while unlocked.

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
lib/store.py:90
Finding
Plaintext Persistent Session Key Lacks Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `lib/store.py:90-107` **Vulnerability Type**: Plaintext cryptographic key storage with insufficient access controls **Risk Level**: High ### Vulnerable Code ```python # Store key in session file (for multi-command usage) self.session_file.write_bytes(self._session_key) print("🔓 Vault unlocked") def lock(self) -> None: """Lock the vault and clear session key.""" self._session_key = None if self.session_file.exists(): self.session_file.unlink() print("🔒 Vault locked") def is_unlocked(self) -> bool: """Check if vault is currently unlocked.""" if self._session_key: return True if self.session_file.exists(): self._session_key = self.session_file.read_bytes() return True return False ``` ### Technical Analysis The derived AES-256 vault key is written directly to `~/.openclaw/vault/session` as plaintext. Unlike the encrypted vault file, the session file is not explicitly assigned mode `0600`. Its effective permissions therefore depend on the process umask. The key remains on disk after the unlocking process terminates and can persist across reboots until `vault lock` is explicitly invoked. Any process or local account able to read this file can decrypt every credential without knowing or brute-forcing the master password. Persisting a raw encryption key is necessary only for the current multi-command session design, not for the core encrypted-storage functionality. The implementation consequently exceeds the minimum exposure required for a memory-only vault session. ### Attack Path 1. The victim executes `vault unlock`. 2. The application derives the AES key and writes it to `~/.openclaw/vault/session`. 3. The victim leaves the vault unlocked or reboots without invoking `vault lock`. 4. A local process or account with permission to read the session file copies its 32-byte contents. 5. The attacker reads `~/.openclaw/vault/vault.enc.json`. ...[truncated 637 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer a memory-only session key and require the user to keep a dedicated process or agent running while the vault is unlocked. 2. If cross-process persistence is required, store the key in an operating-system credential service or protected keyring rather than a regular file. 3. If a file must be used: - Create it atomically with owner-only mode `0600`. - Reject symbolic links and non-regular files. - Verify ownership and permissions before reading it. - Place it in a directory with mode `0700`. - Replace the file atomically rather than following an existing path. 4. Add a short expiration time and remove stale sessions automatically. 5. Clear the session during normal process termination and integrate with operating-system logout or reboot mechanisms where possible. 6. Document that any process running as the vault owner can access secrets while the vault is unlocked. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/vault.py:149
Finding
Unquoted Credential Export Enables Shell Command Injection Through Documented eval Workflow<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vault.py:149-158`; documented invocation in `SKILL.md:123-128` and `SKILL.md:142-148` **Vulnerability Type**: Shell command injection **Risk Level**: High ### Vulnerable Code ```python def cmd_env(args, store: Store, audit: AuditLogger): """Export credentials as environment variables.""" try: credentials = store.list(tag=args.tag) for cred in credentials: value = store.get(cred['name']) print(f"{cred['name']}={value}") audit.log("env", f"tag:{args.tag or 'all'}", {"count": len(credentials)}) return 0 ``` The output is explicitly documented for evaluation by a shell: ```bash eval $(uv run vault env --tag openai) ``` ### Technical Analysis The `env` command concatenates credential names and decrypted values into shell assignment text without validating names or quoting values. The documentation instructs users to pass this output to `eval`. A value containing shell metacharacters, command substitution, semicolons, spaces, or newlines is interpreted as executable shell syntax. Credential names are similarly unrestricted and are not checked against the syntax of environment-variable identifiers. For example, a stored value containing a command substitution can execute a command when the generated assignment is evaluated. Base64 encoding in the vault does not mitigate this issue because the value is decrypted before being printed. ### Attack Path 1. An attacker causes a crafted name or value to be stored in the vault. This could occur through an untrusted migration source, automation that imports secrets, or another process that can invoke the CLI as the user. 2. The crafted value contains shell syntax, such as command substitution or a newline followed by another command. 3. The victim follows the documented integration pattern: ```bash eval $(uv run vault env --tag affected-tag) ``` 4. `cmd_env` decryp ...[truncated 651 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove all documentation recommending `eval $(vault env ...)`. 2. Prefer a command such as `vault run --tag TAG -- command args...` that: - Constructs an environment dictionary internally. - Starts the child process without invoking a shell. - Passes credentials through the child process environment. 3. If shell assignment output must remain available: - Validate every name against `^[A-Za-z_][A-Za-z0-9_]*$`. - Quote every value with a shell-specific escaping function such as `shlex.quote`. - Reject values containing NUL bytes. - Clearly specify the supported shell. 4. Consider a machine-readable output mode, such as JSON, for integrations that can safely construct their own environment. 5. Add tests covering spaces, quotes, semicolons, dollar signs, command substitutions, backticks, and embedded newlines. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
lib/crypto.py:85
Finding
Unsalted Fast Password Verifier Undermines PBKDF2 Brute-Force Resistance<![CDATA[ ## Vulnerability Details **File Location**: `lib/crypto.py:85-96`; verifier use in `lib/store.py:81-88` **Vulnerability Type**: Weak password hashing and offline password oracle **Risk Level**: Medium ### Vulnerable Code ```python def hash_password(password: str) -> str: """Create a verification hash of the master password. This is stored to verify password on unlock, without storing the password itself. Uses SHA-256 (not for key derivation, just verification). Returns: Base64-encoded hash """ return base64.b64encode( hashlib.sha256(password.encode('utf-8')).digest() ).decode('ascii') ``` The stored verifier is checked before PBKDF2 key derivation: ```python # Verify password password_hash = crypto.hash_password(master_password) if password_hash != vault_data["password_hash"]: raise VaultError("❌ Incorrect master password") # Derive and store session key salt = crypto.decode_b64(vault_data["salt"]) self._session_key = crypto.derive_key(master_password, salt) ``` ### Technical Analysis Encryption keys are correctly derived using PBKDF2-SHA256 with 600,000 iterations. However, the vault also stores an unsalted, single-round SHA-256 digest of the same master password. An attacker who obtains the vault file can test password guesses against this fast SHA-256 verifier. The attacker only needs to perform the expensive PBKDF2 operation after discovering a matching password. This bypasses the intended computational cost of PBKDF2 for nearly all guesses and allows identical passwords to have identical verification hashes across vaults. The verifier therefore materially reduces the security provided by the key-derivation function. ### Attack Path 1. The attacker obtains a copy of `vault.enc.json`, such as through a backup leak or local file disclosure. 2. The attacker extracts the Base64-encoded `password_hash`. 3. The attacker performs a dictionary or brute-force attack by calculating one SH ...[truncated 645 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the independent SHA-256 password verifier. 2. Derive the encryption key with the configured password-based KDF and verify it by decrypting a fixed authenticated sentinel stored in the vault. 3. Alternatively, use a separately salted slow password verifier, preferably Argon2id, with parameters appropriate to the deployment environment. 4. Use a constant-time comparison when comparing verifier bytes. 5. Introduce a new vault format version and migrate existing vaults after a successful unlock. 6. Continue requiring strong master passwords; the CLI's current minimum length of eight characters should be increased or supplemented with a strength check. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
lib/audit.py:12
Finding
Audit Log Exposes Credential Metadata Without Enforced Owner-Only Permissions<![CDATA[ ## Vulnerability Details **File Location**: `lib/audit.py:12-16` and `lib/audit.py:35-37` **Vulnerability Type**: Sensitive metadata written with ambient filesystem permissions **Risk Level**: Low ### Vulnerable Code ```python class AuditLogger: """Logs credential access events.""" def __init__(self, log_file: Optional[Path] = None): self.log_file = log_file or DEFAULT_AUDIT_LOG self.log_file.parent.mkdir(parents=True, exist_ok=True) ``` ```python with open(self.log_file, 'a') as f: f.write(json.dumps(entry) + '\n') ``` ### Technical Analysis The plaintext audit log records timestamps, operation names, credential identifiers, tags, expiry data, and other details supplied by command handlers. The implementation creates and appends to the file without explicitly setting owner-only permissions. Consequently, effective access depends on the process umask and existing filesystem state. On a multi-user system, a permissive log can disclose which external services the user accesses and when credentials are retrieved or rotated. This does not expose credential values directly, but credential names and usage patterns are sensitive operational metadata. ### Attack Path 1. The victim invokes vault commands that append records to `~/.openclaw/vault/audit.log`. 2. The log is created under a permissive umask or already has permissive permissions. 3. Another local account or process reads the log. 4. The attacker enumerates credential names, associated tags, expiration dates, access times, and rotation activity. 5. The attacker uses this information to prioritize credential theft, service-specific attacks, or social engineering. ### Impact Assessment The direct impact is confidentiality loss of credential metadata and operational activity. The log does not intentionally contain plaintext credential values, so it does not by itself grant authenticated access to external services. However, the disclosed identifiers and acc ...[truncated 59 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the audit directory with mode `0700`. 2. Create the log atomically with mode `0600`, independently of the current umask. 3. Verify that the log is a regular file owned by the expected user before reading or appending. 4. Avoid following symbolic links when opening the log. 5. Apply `chmod 0600` to existing audit logs during migration. 6. Minimize logged metadata and avoid adding values, command arguments, or other secret-bearing fields to `details`. 7. Add automated tests that verify audit directory and file permissions. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (31)

Credential Access

High
Category
Privilege Escalation
Content
# Lock vault
uv run vault lock

echo "✅ Migration complete. You can now delete .env (but keep a backup!)"
```

**Run migration:**
Confidence
92% confidence
Finding
The guidance says users can delete `.env` 'but keep a backup,' which encourages retention of a plaintext credential backup after migration. Keeping backup copies of `.env` undermines the purpose of moving secrets into encrypted storage and increases the chance of later disclosure via cloud sync, filesystem compromise, or accidental sharing.

Credential Access

High
Category
Privilege Escalation
Content
## Scenario 9: Bulk Export for CI/CD

```bash
# Export all production keys as .env format
$ uv run vault unlock
$ uv run vault env --tag production > /tmp/prod.env
Confidence
97% confidence
Finding
Exporting all production secrets to `/tmp/prod.env` creates a plaintext file in a commonly accessible temporary location, which materially increases exposure risk. Even with later cleanup, secrets may be read by other processes/users, captured by backups, left behind on failure, or persisted in shell workflows and CI artifacts.

Missing User Warnings

High
Confidence
99% confidence
Finding
The debugging workflow instructs users to run `vault get OPENAI_API_KEY` and `echo $OPENAI_API_KEY`, which reveal the full secret in plaintext on screen. This can expose credentials through terminal history tools, logging wrappers, screen sharing, shell session capture, CI logs, or nearby observers, making credential compromise much more likely.

Credential Access

High
Category
Privilege Escalation
Content
**Before:**
```bash
# .env (plaintext, easily leaked)
OPENAI_API_KEY=sk-proj-abc123...
TAVILY_API_KEY=tvly-xyz789...
GITHUB_TOKEN=ghp_def456...
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
**Before:**
```bash
# .env (plaintext, easily leaked)
OPENAI_API_KEY=sk-proj-abc123...
TAVILY_API_KEY=tvly-xyz789...
GITHUB_TOKEN=ghp_def456...
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
**Before:**
```bash
# .env (plaintext, easily leaked)
OPENAI_API_KEY=sk-proj-abc123...
TAVILY_API_KEY=tvly-xyz789...
GITHUB_TOKEN=ghp_def456...
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
**Before:**
```bash
# .env (plaintext, easily leaked)
OPENAI_API_KEY=sk-proj-abc123...
TAVILY_API_KEY=tvly-xyz789...
GITHUB_TOKEN=ghp_def456...
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
**Before:**
```bash
# .env (plaintext, easily leaked)
OPENAI_API_KEY=sk-proj-abc123...
TAVILY_API_KEY=tvly-xyz789...
GITHUB_TOKEN=ghp_def456...
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
97% confidence
Finding
The skill is presented as secure encrypted credential storage, but it also records access events in a plaintext audit log. Even if secret values are omitted, plaintext audit data can reveal credential names, usage patterns, projects, and timing information that may aid attackers in targeting high-value secrets or understanding operator behavior.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill is presented as secure encrypted credential storage, but it also records access events in a plaintext audit log. Even if secret values are omitted, plaintext audit data can reveal credential names, usage patterns, projects, and timing information that may aid attackers in targeting high-value secrets or understanding operator behavior.

Intent-Code Divergence

High
Confidence
95% confidence
Finding
The documentation claims encrypted credential storage, but the implementation stores the active session key unencrypted on disk. This is security-misleading behavior: users may trust the tool to protect secrets more strongly than it actually does, causing unsafe deployment and broader compromise if the session file is accessed.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The vault writes the derived session encryption key directly to a predictable file on disk, which defeats the security boundary of an encrypted credential store. Any local process, malware, backup system, or other user with access to that file can recover the in-memory unlock key and decrypt stored secrets without the master password.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The example explicitly prints part of an API key to terminal output, normalizing the practice of displaying secrets during routine use. Even partial disclosure can leak sensitive prefixes into terminals, shell scrollback, logs, recordings, screenshots, or shared debugging sessions, and the example does not warn users about that risk.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```bash
# Option 1: Store master password in a separate file (less secure)
echo "my-master-password" > ~/.vault_password
chmod 600 ~/.vault_password

# Cron job
0 9 * * * cat ~/.vault_password | /path/to/vault unlock && eval $(/path/to/vault env --tag daily) && /path/to/daily_task.sh
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Session Persistence

Medium
Category
Rogue Agent
Content
### Alias for convenience
```bash
# Add to ~/.zshrc or ~/.bashrc
alias v='uv run vault'
alias venv='eval $(uv run vault env)'
Confidence
90% confidence
Finding
Adding `alias venv='eval $(uv run vault env)'` to persistent shell startup files causes broad, repeated secret-export behavior to become a convenience command available in every shell session. This encourages long-lived exposure of credentials in user environments and increases the chance of accidental export into unrelated commands, subshells, debug sessions, or recorded terminals.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The README explicitly recommends `eval $(uv run vault env --tag ...)`, which injects decrypted secrets into the current shell environment. Environment variables are commonly exposed to child processes, shell history patterns, crash reports, debugging tools, and other local inspection mechanisms, so presenting this workflow without a clear warning encourages users to weaken the protection the vault is meant to provide.

Session Persistence

Medium
Category
Rogue Agent
Content
**Solution:** Double-check your password. If forgotten, you'll need to reinitialize (⚠️ loses all credentials).

### "Vault not initialized"
**Solution:** Run `vault init` to create a new vault.

### Session persists after reboot
**Solution:** Run `vault lock` before shutdown, or add to logout script.
Confidence
88% confidence
Finding
The troubleshooting note confirms that an unlocked session may persist after reboot, which implies decrypted access material or unlock state can survive longer than users expect. That weakens the vault's local security model because a reboot is often assumed to clear sensitive runtime state, and an attacker with later access to the machine could retrieve secrets without re-entering the master password.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill documents shell execution, file reads, and file writes but does not declare any explicit tool scope or permissions boundary. For a credential-management skill, this increases risk because the agent may be granted broader-than-necessary capabilities around sensitive material without clear least-privilege constraints.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The documented use of `eval $(uv run vault env --tag tavily)` injects decrypted secrets into the current shell environment without warning. Environment variables are commonly exposed through shell history, child processes, debug output, crash reports, or process inspection, so this can undermine the confidentiality guarantees of the vault.

Session Persistence

Medium
Category
Rogue Agent
Content
- Session key: `~/.openclaw/vault/session` (temporary, cleared on lock)

### Permissions
- Vault file: `0600` (owner read/write only)
- Session key: deleted on `vault lock`

### Threat Model
Confidence
95% confidence
Finding
The skill explicitly stores a session key on disk until `vault lock`, and notes it is not automatically cleared on reboot. A decryption key persisted on disk weakens the vault's security model because any local compromise, stolen account access, backup capture, or opportunistic file read while the session remains unlocked can bypass the master password entirely.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The documentation encourages direct secret retrieval via `vault get KEY_NAME` and library calls like `store.get(...)` without warning that plaintext secrets may be printed, logged, copied into terminal scrollback, or mishandled by downstream code. For a credential vault, normalizing plaintext retrieval without guardrails materially increases accidental exposure risk.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
Writing the session key to disk without a clear warning or consent increases the likelihood that operators will unknowingly expose decryptable credentials on shared or monitored systems. The absence of a user-facing warning compounds the core design flaw by preventing informed risk decisions.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The list operation reads and returns credential names, tags, and timestamps without requiring the vault to be unlocked. Even if secret values remain encrypted, metadata such as service names, environments, or token labels can reveal sensitive operational details and aid targeted attacks.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The `get` command prints the retrieved secret value directly to stdout, which can leak credentials into logs, transcripts, shell integrations, or calling processes. Because this skill is specifically for secret storage, exposing secrets through the least-controlled output channel is a meaningful security weakness.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The `env` command enumerates stored secrets and prints them in plaintext to stdout as `KEY=VALUE`. In practice, stdout is often captured by terminal history tools, CI logs, shell tracing, wrappers, or parent processes, so this defeats much of the protection expected from an encrypted vault once unlocked.

Static analysis

No suspicious patterns detected.