Back to skill

Security audit

secrets-manager

Security checks for vulnerabilities and agentic risk

Overview

This is a real local secrets tool, but it has several credential-handling flaws and asks for broader permissions than its code appears to need.

Install only if you are comfortable reviewing and fixing the credential-handling issues first. Avoid storing high-value production credentials until the skill stops putting secrets in argv or persistent temp scripts, removes unnecessary shell/elevated permissions, fixes cleanup and --dir behavior, and prevents audit output from printing any secret fragments.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (5)

T09 · Insecure Skill Coding Practices

Error
Location
secrets-manager.js:440
Finding
Audit Command Discloses Plaintext Secret Prefixes<![CDATA[ ## Vulnerability Details **File Location**: `secrets-manager.js:440-444, 460-465` **Vulnerability Type**: Plaintext sensitive-data exposure through logs **Risk Level**: High ### Vulnerable Code ```js if (value.length < 8) { findings.weak.push({ name, length: value.length }); } if (/^(password|admin|root|test|demo|secret|key|token)/i.test(value)) { findings.patterns.push({ name, pattern: value.substring(0, 10) + '...' }); } ``` ```js if (findings.patterns.length > 0) { hasIssues = true; console.log(`\n ⚠️ Weak patterns (${findings.patterns.length}):`); for (const f of findings.patterns) { console.log(` ⚠️ ${f.name}: starts with '${f.pattern}'`); } } ``` ### Technical Analysis The audit operation decrypts every stored secret and, when a value begins with a recognized weak prefix, stores the first ten plaintext characters in the audit result. It subsequently writes those characters to stdout. This violates the documented guarantee that secret values are not logged. The appended `...` does not constitute safe masking: ten characters can disclose an entire short credential or a substantial, operationally useful portion of a longer token. Audit output may be retained in terminal scrollback, agent transcripts, CI logs, journald, or centralized logging systems. ### Attack Path 1. A secret beginning with `password`, `admin`, `root`, `test`, `demo`, `secret`, `key`, or `token` is stored. 2. A user, agent heartbeat, or automated task invokes `--audit`. 3. `auditSecrets()` decrypts the secret. 4. The first ten plaintext characters are written to stdout. 5. An actor with access to captured output or logs obtains credential material. ### Impact Assessment The vulnerability exposes up to ten plaintext characters from affected secrets to every system or user that can observe audit output. For short secrets this may disclose the complete meaningful value. For longer credentials, the disclosed prefix may assist credential identification, ...[truncated 221 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Never place plaintext secret fragments in audit findings or output. - Report only non-sensitive attributes, such as the detected pattern class, secret length, and remediation guidance. - Replace the vulnerable result with data such as: ```js findings.patterns.push({ name, pattern: 'common-prefix' }); ``` - If correlation is required, use a keyed, non-reversible fingerprint and do not expose the key. - Add automated tests that capture stdout and verify that neither complete secrets nor any substring of them appears in audit output. - Review existing audit logs and agent transcripts for previously disclosed credential fragments, then rotate affected credentials. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
secrets-manager.js:509
Finding
Plaintext Secret Injection Files Persist in the Shared Temporary Directory<![CDATA[ ## Vulnerability Details **File Location**: `secrets-manager.js:509-519` **Vulnerability Type**: Unsafe temporary-file handling and persistent plaintext secret storage **Risk Level**: High ### Vulnerable Code ```js } else { // Default: write to a private temp file with restricted permissions and print path const tmpFile = path.join(os.tmpdir(), `secrets-inject-${process.pid}-${Date.now()}.sh`); writeSecure(tmpFile, '#!/bin/sh\n' + result + '\n'); // Track this file so it can be cleaned up on rotate/delete trackTmpInjection(tmpFile, Object.keys(secrets)); console.log(`[secrets-manager] ✅ Injected ${injected} secret(s) into: ${tmpFile}`); console.log(`[secrets-manager] Run with: sh ${tmpFile}`); console.log(`[secrets-manager] File has restricted permissions. Use --cleanup-tmp to remove now, or it will be`); console.log(`[secrets-manager] auto-removed on the next --rotate or --delete for an included secret.`); console.log(`[secrets-manager] To print to stdout instead, use --inject-stdout --confirm-expose`); return tmpFile; } ``` The CLI parser and dispatcher at `secrets-manager.js:547-650` do not recognize or dispatch the advertised `--cleanup-tmp` option. ### Technical Analysis The default injection workflow decrypts credentials and embeds them in a shell script under `os.tmpdir()`, which is commonly a shared `/tmp` directory. Although the file is initially created with mode `0600`, the secret remains in plaintext on disk after use. Cleanup is only triggered indirectly during secret rotation or deletion. The implementation tells users to invoke `--cleanup-tmp`, but no such CLI mode exists. Consequently, users following the displayed cleanup instruction cannot remove tracked files through the advertised interface. The registry also records `Object.keys(secrets)` rather than only the secrets actually substituted. This can cause unrelated rotations or deletions to remove a script, but it does not ensure prompt clea ...[truncated 1095 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Implement and document a functional `--cleanup-tmp` CLI mode that calls `cleanupTmpInjections()`. - Delete each injection file immediately after controlled execution rather than requiring users to execute a persistent script manually. - Prefer passing secrets through protected stdin or inherited file descriptors so plaintext is not written to disk. - If temporary files remain necessary: - Use `fs.mkdtempSync()` to create a private mode-`0700` directory. - Open files with exclusive creation flags to prevent collisions. - Enforce mode `0600` and fail closed if permission enforcement fails. - Apply a short expiration time and clean stale files at every startup. - Remove files in a `finally` block after execution. - Track only placeholder names that were actually resolved. - Add tests for explicit cleanup, startup cleanup, expiry cleanup, and cleanup after execution failure. - Advise users to locate and securely remove existing `secrets-inject-*.sh` files and rotate credentials that may have been retained. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
secrets-manager.js:49
Finding
The --dir Option Does Not Change the Active Secret Storage Directory<![CDATA[ ## Vulnerability Details **File Location**: `secrets-manager.js:49-64, 581-584` **Vulnerability Type**: Security configuration bypass caused by initialization order **Risk Level**: Medium ### Vulnerable Code ```js const WORKSPACE = (() => { if (process.env.SECRETS_DIR) return process.env.SECRETS_DIR; let dir = __dirname; for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(dir, 'MEMORY.md'))) return dir; dir = path.resolve(dir, '..'); } return path.resolve(__dirname, '..', '..'); })(); const DATA_DIR = process.env.SECRETS_DIR || path.join(WORKSPACE, 'memory', 'secrets'); const SECRETS_FILE = path.join(DATA_DIR, 'secrets.json'); const MASTER_KEY_FILE = path.join(DATA_DIR, '.master-key'); const PERMS_FILE = path.join(DATA_DIR, 'permissions.json'); const TMP_INJECTIONS_FILE = path.join(DATA_DIR, '.tmp-injections.json'); ``` ```js else if (arg === '--dir' && i + 1 < args.length) { process.env.SECRETS_DIR = args[++i]; } ``` ### Technical Analysis All storage paths are calculated as module-level constants before `runCLI()` calls `parseCLI()`. The parser later assigns the `--dir` value to `process.env.SECRETS_DIR`, but this does not recalculate `DATA_DIR`, `SECRETS_FILE`, `MASTER_KEY_FILE`, or the registry paths. The option therefore appears to be accepted while storage operations continue to use the directory selected during module initialization. This creates a false security boundary: an operator may believe credentials are being stored on an encrypted or access-controlled filesystem when they are actually written to the default workspace-relative location. ### Attack Path 1. An operator selects a protected directory with `--dir /protected/location`. 2. The module initializes all paths before parsing that option. 3. The parser updates only the environment variable. 4. A store, retrieve, rotation, or deletion operation uses the already initialized default paths. 5. Secret ciphertext and the master key are written to or r ...[truncated 617 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse CLI arguments before calculating any storage paths. - Prefer an explicit configuration object over mutable environment variables: ```js function createPaths(dataDir) { return { dataDir, secretsFile: path.join(dataDir, 'secrets.json'), masterKeyFile: path.join(dataDir, '.master-key'), permsFile: path.join(dataDir, 'permissions.json'), tmpRegistryFile: path.join(dataDir, '.tmp-injections.json') }; } ``` - Pass the resolved path configuration into every storage function. - Resolve the directory to an absolute canonical path and verify that it satisfies the intended policy. - Display the active data directory before sensitive operations when `--dir` is supplied. - Add integration tests that invoke the CLI with `--dir`, then verify that no secret or key file is created in the default location. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
secrets-manager.js:547
Finding
Documented Storage Workflow Exposes Secrets Through Process Arguments<![CDATA[ ## Vulnerability Details **File Location**: `secrets-manager.js:547-548, 603-615` **Vulnerability Type**: Plaintext credential exposure through command-line arguments **Risk Level**: Medium ### Vulnerable Code ```js function parseCLI() { const args = process.argv.slice(2); const result = { mode: 'status', positional: [], flags: { raw: false, injectStdout: false, confirmExpose: false } }; ``` ```js case 'store': { const name = positional[0]; const value = positional[1]; if (!name || !value) { console.log('Usage: secrets-manager.js --store <name> <value>'); } else { storeSecret(name, value); } break; } ``` The documented workflow in `SKILL.md:70-73` explicitly encourages this interface: ```bash node skills/secrets-manager/secrets-manager.js --store openai-key sk-abc123 ``` ### Technical Analysis The secret value is supplied as a positional command-line argument and obtained from `process.argv`. Encryption only occurs after the process starts and parses that argument. Before encryption, the plaintext may be observable through shell history, process-listing facilities, audit frameworks, command telemetry, CI logs, terminal recording, or agent transcripts. Encryption at rest does not mitigate disclosure that occurs through the invocation channel. ### Attack Path 1. A user follows the documented `--store <name> <value>` example. 2. The shell records the complete command or exposes it through process metadata. 3. A local observer, monitoring tool, CI platform, or transcript reader captures the plaintext argument. 4. The secret remains exposed even though the stored copy is subsequently encrypted. ### Impact Assessment Any actor with access to command history, process telemetry, CI logs, or agent transcripts may recover the complete submitted secret. The resulting external privileges are those granted by the credential, potentially including API access, cloud resources, source repositori ...[truncated 198 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Add a stdin-based storage mode and make it the documented default: ```bash printf '%s' "$SECRET" | node secrets-manager.js --store-stdin openai-key ``` - For interactive use, read the value from a no-echo terminal prompt. - Support protected file descriptors or restricted input files for automation. - Deprecate plaintext positional secret arguments and display a warning when the legacy form is used. - Ensure errors, debug messages, and tests never echo input values. - Update all examples in `SKILL.md` and `README.md`. - Rotate credentials that may already have appeared in shell history, CI output, or agent transcripts. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
SKILL.md:1
Finding
Skill Requests Shell, Execution, and Elevated Permissions That Its Implementation Does Not Use<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:1-6` **Vulnerability Type**: Excessive capability declaration and least-privilege violation **Risk Level**: Medium ### Vulnerable Code ```yaml --- name: secrets-manager version: 1.1.19 description: Encrypted local secret store for OpenClaw agents. AES-256-GCM authenticated encryption with per-secret random IVs, master key in restricted-permission .master-key file. Store, retrieve, rotate, and audit secrets. Safe command injection (writes to temp file by default; --inject-stdout requires --confirm-expose). Master key is recoverable from .master-key file; losing it makes stored secrets unrecoverable. permissions: ["fs", "env", "exec", "shell", "elevated"] --- ``` The package metadata also requests elevation at `clawhub.yaml:26-27`: ```yaml - name: elevated description: Set restricted permissions on secret files; required only when $SECRETS_DIR is outside $HOME. Least-privilege: elevation used solely for file permission enforcement, not for reading or writing secrets. ``` ### Technical Analysis The reviewed implementation uses Node.js filesystem, environment, operating-system, and cryptographic APIs. It does not import `child_process`, execute subprocesses, invoke a shell, or implement an elevation mechanism. Ordinary owner-controlled `chmod` operations do not require elevated privileges. If the current user cannot write or change permissions in a selected directory, elevation would also be needed to establish ownership or create files there; the implementation does not perform such a controlled privileged operation. Requesting `exec`, `shell`, and `elevated` therefore exceeds the demonstrated functional requirements and weakens the least-privilege boundary. ### Attack Path 1. A user installs or enables the Skill and approves its declared capabilities. 2. The Skill receives shell, execution, and elevated authority despite not requiring them for its current implementation. 3. If the Skill ...[truncated 729 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove `exec`, `shell`, and `elevated` from the Skill frontmatter. - Restrict filesystem access to the configured private data directory and the specifically managed temporary directory. - Retain only environment access needed for `SECRETS_DIR` and `SECRETS_MASTER_KEY`. - Fail closed when restrictive permissions cannot be established instead of claiming elevation is required. - If privileged storage must be supported, implement it as a separate, narrowly scoped helper with explicit user confirmation, fixed allowed operations, canonical-path validation, and no arbitrary command execution. - Keep `SKILL.md` and `clawhub.yaml` permission declarations consistent. - Add a release check that compares declared capabilities with APIs actually used by the implementation. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (14)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
This is a second report of the same core issue: the file presents itself as a secure local secret store, but the analysis says it is actually a test harness with no implemented cryptography or secret-management safeguards. A deceptive or inaccurate security posture around secret storage is itself a serious vulnerability because it causes unsafe operational decisions based on false assurances.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
This is a second report of the same core issue: the file presents itself as a secure local secret store, but the analysis says it is actually a test harness with no implemented cryptography or secret-management safeguards. A deceptive or inaccurate security posture around secret storage is itself a serious vulnerability because it causes unsafe operational decisions based on false assurances.

Ae1

High
Category
analysis-evasion
Content
| `run-cli` | CLI invocation | Invoke `secrets-manager.js` for store/get/rotate/audit operations |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
| Approach | Encryption | Setup | Audit | Rotation | Recovery |
|----------|-----------|-------|-------|----------|----------|
| Environment vars | None | Medium | None | Manual | N/A |
| .env files | None | Low | None | Manual | N/A |
| **Secrets Manager** | **AES-256-GCM** | **None** | **Auto** | **Auto** | **With .master-key** |
| Vault service | Various | High | Auto | Auto | Yes |
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
* Secrets Manager — Local encrypted secret store for OpenClaw agents
 *
 * Storage: AES-256-GCM encryption with a per-install master key.
 *   - secrets.json: { name: { iv, ct, tag, created, updated, rotationDays, lastRotated, rotationCount } }
 *   - .master-key: 32 random bytes, restricted permissions (owner-only)
 *   - If .master-key is lost, all stored secrets become unrecoverable
 *
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
* Secrets Manager — Local encrypted secret store for OpenClaw agents
 *
 * Storage: AES-256-GCM encryption with a per-install master key.
 *   - secrets.json: { name: { iv, ct, tag, created, updated, rotationDays, lastRotated, rotationCount } }
 *   - .master-key: 32 random bytes, restricted permissions (owner-only)
 *   - If .master-key is lost, all stored secrets become unrecoverable
 *
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
*   - Master key auto-generated on first store, restricted permissions (owner-only)
 *   - Per-secret random 12-byte IV
 *   - All write files use atomic temp+rename, restricted permissions on secrets.json + .master-key
 *   - secrets.json includes auth tag → tampering causes decrypt to return null
 *   - NEVER print raw secret or substituted command unless explicitly confirmed
 *
 * Permissions: filesystem (memory/secrets/), env (SECRETS_DIR override, SECRETS_MASTER_KEY override)
Confidence
88% confidence
Finding
Allowing the master key to be supplied via the SECRETS_MASTER_KEY environment variable creates a realistic secret exposure path because environment variables are often inherited by child processes, surfaced in debugging tools, and sometimes captured by logs or orchestration metadata. In a skill with exec/shell/elevated permissions, this broadens the attack surface around the single key that decrypts the entire store.

Credential Access

High
Category
Privilege Escalation
Content
})();

const DATA_DIR = process.env.SECRETS_DIR || path.join(WORKSPACE, 'memory', 'secrets');
const SECRETS_FILE = path.join(DATA_DIR, 'secrets.json');
const MASTER_KEY_FILE = path.join(DATA_DIR, '.master-key');
const PERMS_FILE = path.join(DATA_DIR, 'permissions.json');
const TMP_INJECTIONS_FILE = path.join(DATA_DIR, '.tmp-injections.json');
Confidence
93% confidence
Finding
SECRETS_DIR is taken directly from the environment and used as the storage path for both secrets.json and .master-key without validation that it points to a private, trusted directory. In this skill context, an attacker who can influence the environment can redirect secret storage to a world-readable location, a shared mount, or an attacker-controlled path, causing disclosure or tampering of the encrypted store and key material.

External Transmission

Medium
Category
Data Exfiltration
Content
- Tools requested: exec process read write 

## Network footprint
https://api.openai.com/v1/chat

## Side effects
- Reads: SKILL.md
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
- Tools requested: exec process read write 

## Network footprint
https://api.openai.com/v1/chat

## Side effects
- Reads: SKILL.md
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
// ─── CREDENTIAL PATH MANAGEMENT ───────────────────────────────────────────
// These paths are dynamically managed by the skill itself — they are not
// hardcoded secrets. The skill creates, encrypts, and protects these files
// with chmod 0600 on first use. Override via SECRETS_DIR env var.
// All paths resolve relative to the private data directory, not absolute system paths.

const WORKSPACE = (() => {
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Intent-Code Divergence

Medium
Confidence
91% confidence
Finding
The manifest says the skill can 'rotate' secrets in an encrypted local store, which ordinarily means re-encrypting or replacing cryptographic material while preserving the stored secret unless explicitly updated. This test instead asserts that after `rotateSecret('rotate-test')`, the retrieved plaintext must differ from the original value, documenting behavior where rotation mutates the secret value itself rather than just its protection state.

Intent-Code Divergence

Low
Confidence
77% confidence
Finding
The README frames secret exposure as gated around explicit stdout-oriented flags, saying injection 'NEVER prints secrets to stdout unless you opt in.' However, the same document later advertises `SM.getSecret()` as returning the plaintext secret value directly, which weakens the earlier safety framing by exposing secrets through normal API use without the same opt-in language. This is a documentation-level intent inconsistency rather than a code-level permission issue.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The module documentation advertises distinct filtered modes for `--audit --expired` and `--audit --stale` (L018-L020), and the CLI passes `'expired'` or `'stale'` into `auditSecrets` (L642-L646). However, `auditSecrets(filter = null)` never uses its `filter` parameter and always computes and prints all categories, so the documented behavior is contradicted by the actual code.

Static analysis

No suspicious patterns detected.