Back to skill

Security audit

Openclaw

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent for onboarding a Plenty of Bots agent, but it needs review because it handles long-lived bot credentials, allows arbitrary auth/registration endpoints, and encourages recurring autonomous dating-platform messaging.

Review this carefully before installing. Use it only if you trust Plenty of Bots and keep authentication and registration pointed at https://plentyofbots.ai/api. Do not use --api-base with production credentials, protect the credentials file as a long-term secret, rotate keys if they were printed into shared logs, and enable heartbeat messaging only with explicit owner approval and clear outreach limits.

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/auth.js:52
Finding
Authentication Relay and Ed25519 Signing Oracle Through Unrestricted API Base<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth.js:52-105`, `scripts/auth.js:255-263` **Vulnerability Type**: Unrestricted authentication endpoint / challenge relay **Risk Level**: High ### Vulnerable Code ```javascript export async function authenticate(botProfileId, privateKeyBase64, apiBase = DEFAULT_API_BASE) { if (!botProfileId || typeof botProfileId !== 'string') { throw new Error('botProfileId is required'); } if (!privateKeyBase64 || typeof privateKeyBase64 !== 'string') { throw new Error('privateKey (base64) is required'); } // Step 1: Request challenge let challengeRes; try { challengeRes = await fetch(`${apiBase}/bots/auth/challenge`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ botProfileId }), }); } catch (error) { throw new Error(`Network error during challenge: ${error.message}`); } // ... const { nonceId, nonce } = challengeData; if (!nonceId || !nonce) { throw new Error('Unexpected challenge response: missing nonceId or nonce'); } // Step 2: Sign nonce const nonceBytes = Buffer.from(nonce, 'base64'); const privateKeyBytes = Buffer.from(privateKeyBase64, 'base64'); const signature = await signAsync(nonceBytes, privateKeyBytes); const signatureBase64 = Buffer.from(signature).toString('base64'); // Step 3: Verify let verifyRes; try { verifyRes = await fetch(`${apiBase}/bots/auth/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ botProfileId, nonceId, signature: signatureBase64 }), }); } catch (error) { throw new Error(`Network error during verify: ${error.message}`); } } ``` ```javascript case '--api-base': result.apiBase = args[++i]; break; ``` ### Technical Analysis The authentication implementation accepts an unrestricted `apiBase` and signs any Base64 value returned as `nonce`. It does not require HTTPS ...[truncated 2616 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `--api-base` from production-facing authentication commands and use the fixed API endpoint. 2. If alternate endpoints are required for development, parse the URL and enforce: - `https:` only; - an explicit hostname allowlist; - an exact expected path prefix; - no embedded username or password; - no unapproved ports. 3. Require a separate, explicit development-mode flag before permitting non-production endpoints. Display a prominent warning and prohibit use of production credentials in that mode. 4. Add domain separation to the signed payload, for example by signing a canonical structure containing: - protocol identifier; - expected origin; - bot profile ID; - nonce ID; - nonce; - expiration time. 5. Validate the nonce encoding, decoded size, nonce ID format, and challenge expiration before signing. 6. Where supported by the server, bind challenges to a client session or ephemeral key so that a challenge cannot be relayed by another origin. 7. Add tests proving that HTTP URLs, arbitrary hosts, malformed URLs, user-info URLs, and unapproved ports are rejected. 8. Revoke tokens and rotate bot keys if authentication has previously been performed against an untrusted API base. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/register.js:78
Finding
Unvalidated Claim URL Enables Phishing Through a Spoofed Registration API<![CDATA[ ## Vulnerability Details **File Location**: `scripts/register.js:78-130` **Vulnerability Type**: Untrusted endpoint and unvalidated security-sensitive URL **Risk Level**: Medium ### Vulnerable Code ```javascript export async function registerBot(options, apiBase = DEFAULT_API_BASE) { const { handle, displayName, bio, publicKey, ...extensionFields } = options; // Validate required fields if (!handle || typeof handle !== 'string') { throw new Error('handle is required (3-30 chars, lowercase alphanumeric + underscore)'); } if (!displayName || typeof displayName !== 'string') { throw new Error('displayName is required (1-100 chars)'); } if (!publicKey || typeof publicKey !== 'string') { throw new Error('publicKey is required (base64-encoded Ed25519 public key, 44 chars)'); } const body = { handle, displayName, publicKey, ...extensionFields }; if (bio !== undefined) { body.bio = bio; } let response; try { response = await fetch(`${apiBase}/bots/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); } catch (error) { throw new Error(`Network error: ${error.message}`); } const text = await response.text(); let data; try { data = JSON.parse(text); } catch { throw new Error(`Invalid JSON response (HTTP ${response.status}): ${text}`); } // ... const claimUrl = data.claimUrl; const botProfileId = data.bot?.profile?.id; const expiresAt = data.expiresAt; if (!claimUrl || !botProfileId) { throw new Error(`Unexpected response format: missing claimUrl or bot.profile.id`); } return { claimUrl, botProfileId, expiresAt }; } ``` ### Technical Analysis `registerBot` accepts an arbitrary API base and trusts the returned `claimUrl` without parsing or validating its scheme, origin, or path. The Skill documentation then instructs the agent to present that URL to the owner for sign-in and bot activatio ...[truncated 1854 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Fix production registration to `https://plentyofbots.ai/api` or enforce an explicit HTTPS hostname allowlist. 2. Parse the returned claim URL with `new URL()` and require: - `https:`; - hostname exactly `plentyofbots.ai`; - the expected claim path; - no embedded credentials; - no unexpected port. 3. Reject malformed URLs, lookalike domains, subdomain suffix tricks, and redirects to unapproved origins. 4. Replace the `...extensionFields` spread with an explicit allowlist of supported profile fields. 5. Validate all documented length, type, and enum constraints locally before transmission. 6. Make development or staging endpoints opt-in and visibly mark claim URLs from those environments as non-production. 7. Add tests that reject attacker-controlled API origins and claim URLs. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/keygen.js:39
Finding
Private Keys Can Be Exposed Through Standard Output and Process Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/keygen.js:39-43`, `scripts/auth.js:255-260`, `SKILL.md:145-155`, `SKILL.md:202-207` **Vulnerability Type**: Sensitive data exposure through command-line and output channels **Risk Level**: Medium ### Vulnerable Code ```javascript if (isMainModule) { try { const keypair = await generateKeypair(); console.log(JSON.stringify(keypair, null, 2)); } catch (error) { console.error('Error generating keypair:', error.message); process.exit(1); } } ``` The printed object contains both key components: ```javascript return { privateKey, publicKey }; ``` The authentication CLI accepts the private key directly as an argument: ```javascript case '--private-key': result.privateKey = args[++i]; break; ``` The documented workflow encourages both behaviors: ```bash node ${SKILL_DIR}/scripts/keygen.js ``` ```json { "privateKey": "<base64-encoded private key>", "publicKey": "<base64-encoded public key>" } ``` ```bash node ${SKILL_DIR}/scripts/auth.js \ --profile-id <bot_profile_id> \ --private-key <private_key_base64> ``` ### Technical Analysis The key-generation CLI writes the raw private key to standard output. Standard output is frequently captured by agent transcripts, orchestration logs, CI systems, shell redirection, terminal recording, or diagnostic tooling. This expands secret exposure beyond the intended credential file. The authentication CLI also permits the raw private key in `process.argv`. Depending on the operating system and process isolation settings, command-line arguments may be visible through process inspection tools, `/proc`, monitoring agents, audit logs, shell history, or job metadata. These channels are unnecessary once a credentials-file workflow exists. The behavior increases exposure beyond the minimum privilege required to authenticate the bot. ### Attack Path 1. A user or agent follows the documented key-generation command. 2. The private key ...[truncated 951 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make secure file creation the default key-generation behavior: - create a new credentials file with exclusive creation; - set mode `0600`; - print only the public key and destination path. 2. Do not print the private key unless the user supplies an explicit high-risk export flag and confirms the action interactively. 3. Remove `--private-key` from the normal CLI. Read the key from: - a protected credentials file; - standard input without echo; - an inherited file descriptor; or - an operating-system secret store. 4. Avoid environment variables for long-lived secrets because they may also be exposed through process inspection and diagnostics. 5. Redact private keys and bot tokens from logs and agent transcripts. 6. Document secure backup and key-rotation procedures. 7. Treat any key previously captured in shared logs or transcripts as compromised and rotate it. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/auth.js:158
Finding
Credential File Updates Do Not Enforce Owner-Only Permissions on Existing Files<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth.js:158-166` **Vulnerability Type**: Insecure credential-file permission handling **Risk Level**: Medium ### Vulnerable Code ```javascript export async function saveCredentials(filePath, credentials) { const dir = dirname(filePath); await mkdir(dir, { recursive: true }); await writeFile(filePath, JSON.stringify(credentials, null, 2) + '\n', { mode: 0o600 }); } ``` ### Technical Analysis Passing `mode: 0o600` to `writeFile` applies the requested mode when a new file is created. It does not reliably change the permissions of a file that already exists. Consequently, an existing credentials file created with permissive permissions such as `0644` can remain readable by other local users after `saveCredentials` writes the private key and refreshed bot token into it. The documentation tells users to run `chmod 600`, but the code should enforce this invariant itself. Security-sensitive storage must not depend on a separate manual step that may be omitted. The function also writes directly to the final file rather than using an atomic, exclusive temporary-file replacement. That is primarily a reliability and hardening concern, but it further weakens handling of long-lived credentials. ### Attack Path 1. A credentials file already exists with group- or world-readable permissions, for example because it was manually created under a permissive umask or restored from a backup. 2. The user invokes the authentication script with that credentials file. 3. Token refresh calls `saveCredentials`, which writes the private key and token while supplying `mode: 0o600`. 4. Because the file already exists, its permissive mode may remain unchanged. 5. Another local account or process with read access obtains the private key and bot token. 6. The credentials are used to impersonate the bot. ### Impact Assessment An attacker with local read access to the permissive file can obtain both the cached token and ...[truncated 380 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Explicitly enforce permissions after every creation or update: ```javascript import { chmod } from 'node:fs/promises'; await writeFile(filePath, serialized, { mode: 0o600 }); await chmod(filePath, 0o600); ``` 2. Prefer an atomic update: - create a temporary file in the same directory using exclusive creation; - set mode `0600`; - write and synchronize the data; - atomically rename it over the target; - verify the final file's ownership and mode. 3. Reject symbolic-link credential targets where feasible, or open files with platform-appropriate no-follow and exclusive flags. 4. Verify that the parent directory is owned by the expected account and is not group- or world-writable. 5. Consider setting the credentials directory to `0700`. 6. Add tests that begin with an existing `0644` file and confirm that it becomes `0600` after update. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
Findings (12)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The declared description presents a full dating-platform capability set: registering bots, authenticating with Ed25519, discovering profiles, and having conversations. The supplied code chunk does not implement those platform behaviors. Instead, it is a unit test focused narrowly on verifying that generateKeypair() returns valid, unique base64-encoded 32-byte keys. While key generation could support an Ed25519 authentication system, this file itself only tests key formatting and uniqueness. That is a materially different and much narrower purpose than the declared end-user platform functionality, so this chunk does not accurately represent the described skill behavior.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The code is narrowly focused on registering a bot account/profile. It validates handle/displayName/publicKey, posts registration data to the Plenty of Bots API, parses the registration response, and supports CLI invocation. The declared description presents a broader platform capability set: registering bots, authenticating with Ed25519, discovering profiles, and having conversations. In this code chunk, only the registration portion is present. There is no implementation for profile search/discovery, no messaging or conversation functionality, and no cryptographic authentication or signing flow beyond passing a public key as a field. Because the declared purpose materially overstates what this specific code chunk does, this is a description-behavior mismatch.

Credential Access

High
Category
Privilege Escalation
Content
This automatically:
- Generates your Ed25519 keypair
- Registers you on the platform
- Saves credentials to `~/.pob/credentials.json`

### 3. Give the Claim URL to Your Owner
Confidence
89% confidence
Finding
The skill directs saving credentials, including bot secrets, to a predictable plaintext path in the user's home directory. Even though local storage is sometimes necessary, placing reusable authentication material on disk without stronger secret-management controls increases the risk of theft by other local agents, malware, misconfigured backups, or accidental disclosure.

Credential Access

High
Category
Privilege Escalation
Content
/**
 * @typedef {Object} AuthResult
 * @property {string} botToken - Opaque access token
 * @property {string} expiresAt - ISO 8601 datetime when token expires
 * @property {string[]} [scopes] - Token scopes
 */
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
if (!botProfileId || !privateKey) {
      console.error('Usage:');
      console.error('  node auth.js --profile-id <uuid> --private-key <base64>');
      console.error('  node auth.js --credentials <path-to-credentials.json>');
      process.exit(1);
    }
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill clearly instructs network access to external APIs and periodic outbound communication, but the manifest does not declare any explicit tool scope such as allowed-tools or permissions. That makes the skill's operational reach less transparent to users and hosting platforms, increasing the chance that network-capable behavior is approved or invoked without informed consent.

Session Persistence

Medium
Category
Rogue Agent
Content
Look at your SOUL.md or PROFILE.md to find:
- Your **name** (for displayName)
- Your **bio** or description
- Create a **handle** from your name (lowercase, underscores instead of spaces)

### 2. Generate Keypair and Register
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.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill instructs storing a private key and cached bot token on disk, but does not prominently warn the operator about the sensitivity of these secrets or the consequences of disk compromise. If another local user, process, backup system, or agent accesses the file, an attacker could impersonate the bot until the token expires and continue reauthenticating with the private key.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Set file permissions to owner-only:
```bash
chmod 600 ~/.openclaw/credentials/pob-<handle>.json
```

### Step 10: Confirm Ready
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The heartbeat section directs the bot to autonomously check inboxes, discover profiles, and proactively start or re-engage conversations on a schedule, but it does not clearly warn users that enabling the skill may cause unsupervised outreach. In a social or dating context, undisclosed autonomous messaging increases consent, reputational, spam, and policy-abuse risks.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The CLI prints the full authentication result, which includes the botToken, directly to stdout. Tokens commonly end up in terminal scrollback, shell history wrappers, CI logs, or process capture systems, allowing anyone with access to those logs to reuse the token until expiry.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"keygen": "node scripts/keygen.js"
  },
  "dependencies": {
    "@noble/ed25519": "^2.2.3"
  }
}
Confidence
84% confidence
Finding
The dependency uses a caret range (^2.2.3), which allows newer minor and patch releases to be installed over time. That creates a supply-chain integrity risk because different environments may resolve different versions, and a compromised or breaking upstream release could be pulled in without explicit review; in a skill handling Ed25519 cryptography, dependency trust matters more than in non-security-sensitive code.

Static analysis

Detected: suspicious.exposed_secret_literal

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/auth.js:285