Back to skill

Security audit

PowPow Simple EN - Publish posts & digital humans to the public map

Security checks for vulnerabilities and agentic risk

Overview

The skill is purpose-aligned but asks for PowPow credentials, stores a session token locally, and can publish/delete public content or spend badges with approvals enforced mainly by instructions rather than the scripts themselves.

Install only if you are comfortable letting this skill handle your PowPow login and keep a local session token. Treat the session file like a credential, review every draft and image list before publishing, and be aware that anyone or any agent process able to run the scripts with that session can publish, delete your PowPow posts, or create a badge-consuming digital human unless the host adds its own approval controls.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/publish.js:127
Finding
Public post publication and local image upload lack an executable confirmation gate## Vulnerability Details **File Location**: `scripts/publish.js`, lines 127–173 **Vulnerability Type**: Missing authorization confirmation for public publication and file upload **Risk Level**: High ### Complete Code Snippet ```js // Deferred local-image upload: this is the ONLY moment the images // leave the user's machine (compose.js keeps everything local). try { content = await uploadLocalImages(content, htmlPath); } catch (err) { if (err instanceof SessionExpiredError) { console.error(`❌ ${err.message}`); process.exitCode = 2; return; } console.error(`\n❌ Image upload failed: ${err.message}`); console.error(' Nothing was posted. Fix the issue and re-run publish.js ' + '(already-uploaded images are reused, not re-uploaded).'); process.exitCode = 1; return; } const dhInfo = extractDigitalHuman(content); if (dhInfo) console.log(`Digital Human: ${dhInfo.name} | ID: ${dhInfo.id}`); const locInfo = extractLocation(content); if (locInfo) console.log(`Location: ${locInfo.name} (${locInfo.lng}, ${locInfo.lat})`); // Guard: interactive components must use the editor's exact HTML structure, // otherwise the web frontend renders them as plain text. const formatIssues = validateEditorFormat(content); if (formatIssues.length > 0) { console.error('\n❌ Component HTML does not match the platform editor format:'); formatIssues.forEach(i => console.error(` - ${i}`)); console.error(' Fix: build the post with html-formatter.js helpers (formatPostHTML /'); console.error(' createDigitalHumanSpan / createLocationSpan), not hand-written spans.'); process.exitCode = 1; return; } // Payload mirrors the web editor's confirmPublish() exactly: // interactive components live inside the rich `content` HTML only. // Do NOT send contentItems here - a partial items list makes the frontend // switch to structured rendering and drop the post text. const postData = { type: 'text', content, ...(locInfo ? { lng: locInfo.lng ...[truncated 2578 chars]
Remediation
## Remediation Suggestions - Require an executable confirmation gate before any image upload or publication. - Generate a short-lived approval artifact after displaying the final preview. - Bind the approval to: - The authenticated account identifier. - A cryptographic digest of the final HTML. - The complete manifest and local image list. - The location and exposure setting. - An expiration time and single-use nonce. - Validate and consume the approval artifact inside `publish.js`; reject execution if it is missing, expired, reused, or does not match the final draft. - Place the confirmation check before `uploadLocalImages()` so no local file leaves the machine before approval. - For direct interactive use, display the account, destination, location, and image list and require an explicit confirmation. - Provide a dry-run mode that performs validation and displays the proposed operation without uploading or publishing.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/create-digital-human.js:155
Finding
Paid digital-human creation relies only on instruction-layer confirmation## Vulnerability Details **File Location**: `scripts/create-digital-human.js`, lines 155–194 **Vulnerability Type**: Missing authorization confirmation for a paid and public operation **Risk Level**: High ### Complete Code Snippet ```js const session = requireSession(); // Badge pre-check: creating costs BADGES_REQUIRED badges and deletion does // NOT refund them - fail fast before uploading anything. if (session.userId) { const bal = await api('GET', `/api/badges/balance?userId=${session.userId}`); const current = bal.balance ? bal.balance.balance : 0; if (current < BADGES_REQUIRED) { throw new Error(`Badge balance too low: ${current} available, ${BADGES_REQUIRED} required. Earn badges on the platform first (see https://global.powpow.online).`); } console.log(` 💰 Badge balance: ${current} (creating a digital human consumes ${BADGES_REQUIRED})`); } else { console.warn(' ⚠️ No userId in session - skipping local badge pre-check (server will still reject if insufficient).'); } const { avatarUrl, referenceImageUrl } = await resolveAvatar(opts, description); console.log(' ⏳ Creating digital human...'); const payload = { name: opts.name.trim(), description: description.trim(), avatarUrl, lng, lat, }; if (opts.locationName && opts.locationName.trim()) payload.locationName = opts.locationName.trim(); if (referenceImageUrl) payload.referenceImageUrl = referenceImageUrl; const body = await api('POST', '/api/digital-humans', payload); ``` The file also explicitly acknowledges that confirmation is external to the executable: ```js * Cost warning: this consumes 2 badges. The agent must confirm with the * user BEFORE running this script (see SKILL.md workflow). ``` ### Technical Analysis Creating a digital human consumes two badges without a refund on deletion and publishes the resulting entity on the public map. `SKILL.md` lines 356–359 require explicit user confirmation, but the script itself does not enforce that requirement. The ba ...[truncated 1837 chars]
Remediation
## Remediation Suggestions - Enforce confirmation within the executable before image upload or paid creation. - Bind approval to the authenticated account, exact two-badge cost, name, persona digest, avatar source, coordinates, location name, and expiration behavior. - Use a short-lived, single-use approval token generated only after the final operation summary is shown to the user. - Reject execution when the approval token is absent, expired, reused, or does not match the supplied parameters. - Perform the confirmation check before `resolveAvatar()` to prevent unapproved image uploads. - Keep the balance check, but treat it only as an affordability check rather than proof of consent. - Add a dry-run mode that returns the complete proposed payload and cost without uploading an image or creating the entity.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/delete-post.js:10
Finding
Authenticated post deletion executes without final confirmation or dry-run## Vulnerability Details **File Location**: `scripts/delete-post.js`, lines 10–18 **Vulnerability Type**: Missing authorization confirmation for a destructive operation **Risk Level**: Medium ### Complete Code Snippet ```js async function main() { const [, , postId] = process.argv; if (!postId) { console.error('Usage: node delete-post.js <post-id>'); process.exit(1); } try { const body = await api('DELETE', `/api/posts/${postId}`); console.log(`✅ Post deleted: ${postId}`); ``` ### Technical Analysis The script accepts a post ID and immediately converts it into an authenticated `DELETE` request. It provides no preview, interactive confirmation, explicit confirmation parameter, or dry-run mode. Server-side JWT ownership checks reportedly restrict deletion to the authenticated user's own posts. That is useful tenant isolation, but it does not establish that the account owner authorized deletion of the selected post at that time. Consequently, any caller capable of invoking the script in the authenticated Skill context can trigger the destructive operation. The attacker-controlled point is the post ID argument. The crossed trust boundary is from a local command-line value into an authenticated deletion of persistent user content. ### Attack Path 1. A valid PowPow session exists in the state directory. 2. An agent, automation workflow, or untrusted instruction with script invocation capability obtains or supplies the ID of one of the authenticated user's posts. 3. It runs `node scripts/delete-post.js <post-id>`. 4. The script immediately sends `DELETE /api/posts/<post-id>` with the stored bearer token. 5. The user's post is deleted without a script-enforced final confirmation. The server's ownership check prevents this path from deleting another user's post, but it does not prevent unauthorized deletion within the current account. ### Impact Assessment Successful exploitation can delete a post belonging to the authenticated PowPow ...[truncated 270 chars]
Remediation
## Remediation Suggestions - Default to a non-destructive preview that retrieves and displays the target post's metadata. - Require explicit interactive confirmation showing the account and exact post before sending the deletion request. - For non-interactive automation, require a short-lived, single-use confirmation artifact bound to the account and post ID. - Add a `--dry-run` mode and make it the default behavior. - Consider requiring an explicit destructive flag in addition to the approval artifact, while ensuring that a flag alone is not treated as proof of user consent. - Record a local audit event containing the post ID, account ID, approval identifier, and timestamp without logging the bearer token.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (32)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
publishing workflow (login → self-check → geocode → match → compose →
  publish → verify), not an independent hidden tool.
- `delete-post.js` deletes only the logged-in user's own post
  (`DELETE /api/posts/{id}`, JWT-scoped, enforced server-side) and is
  documented in SKILL.md Step 8 for test cleanup. This capability is also
  declared in the skill description.
- No secrets are bundled. `config.json` ships defaults only. The session JWT
Confidence
80% 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).

Ae1

High
Category
analysis-evasion
Content
node scripts/match-digital-human.js "<topic>" --limit 3 --json # ranked suggestions
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/match-digital-human.js "<topic>" --limit 3 --json # ranked suggestions
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/list-digital-humans.js "<name>" --json # search by name
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/list-digital-humans.js "<name>" --json # search by name
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/geocode.js "地坛公园" --limit 5 # add --city 北京 to narrow
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **Automatic image search** (only if an Unsplash key is configured): `--image search:<english keywords>` (or `node scripts/search-image.js "<keywords>"`)
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
**Mandatory: always use `scripts/compose.js` to assemble post HTML. Never hand-write component spans, never write glue code to bypass html-formatter.js.**
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
**Mandatory: always use `scripts/compose.js` to assemble post HTML. Never hand-write component spans, never write glue code to bypass html-formatter.js.**
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/verify.js <post-id>
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/delete-post.js <post-id>
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/create-digital-human.js \
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Tool Parameter Abuse

High
Category
Tool Misuse
Content
- `POST /api/upload/post-image` — local image upload (multipart `file`; login required)
- `POST /api/posts` — create post (login required; server enforces limits & moderation)
- `GET  /api/posts/{id}` — verify a post
- `DELETE /api/posts/{id}` — delete own post (test cleanup)
- `POST /api/digital-humans` — create a digital human (login required;
  consumes 2 badges, expires after 30 days)
- `POST /api/digital-humans/generate-avatar` — AI avatar from the persona
Confidence
80% 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).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
/**
 * Delete one of your own posts (DELETE /api/posts/[id], JWT-scoped).
 * Useful for cleaning up test posts. You can only delete your own posts.
 *
 * Usage: node delete-post.js <post-id>
Confidence
90% 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).

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill performs network access, reads environment variables, and manages authentication/session state, but it does not declare any explicit tool scope or permissions boundary. That mismatch weakens sandboxing and reviewability: a host may grant broader capabilities than users or reviewers expect, increasing the blast radius if the skill is modified or abused.

Session Persistence

Medium
Category
Rogue Agent
Content
---
name: "PowPow Simple EN - Publish posts & digital humans to the public map"
description: Publish posts/travelogues to PowPow (global.powpow.online), and create digital humans pinned to the public map. Triggers when the user wants to publish travel content (photos / travelogue / trip stories) to PowPow, e.g. "publish a PowPow post", "post to PowPow feed", "turn my travel photos into a travelogue on PowPow", "post these photos to PowPow", "发一篇 PowPow 帖子", "把这次旅行的照片发到泡泡"; also triggers when the user wants to create/publish a digital human onto the map, e.g. "create a digital human", "turn someone into a digital human on the map", "publish a digital human". Requires a PowPow account (register first if none). Included auxiliary capabilities (all are parts of the publish/create flow above) — account login & session management, environment self-check, place-name resolution to coordinates, digital-human search & topic matching, image search & upload, post composition & publishing, post-publish verification, deleting one's own posts (test cleanup only, JWT-scoped to the logged-in user's own posts). Does not publish to other social platforms; no subscriptions or marketing features.
version: 1.0.0
metadata:
  openclaw:
Confidence
83% confidence
Finding
The skill advertises account login and session management, and later confirms that JWT session tokens are cached locally in a state directory. Persisting authentication material across runs increases the risk of token theft, cross-user mix-ups on shared hosts, and unintended continued access if the environment is compromised.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger phrases for digital-human creation are broad enough to match ordinary creative or conversational requests, which can route users into a flow that asks for login credentials and may spend platform resources. In this skill's context, false triggering is more dangerous because the workflow can lead to public posting or a 2-badge irreversible action.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
1. **What I can do**: turn your photos or material into a travelogue and publish it to PowPow for you.
  2. **What the result looks like**: a post can carry digital-human tags (a red capsule — tap the avatar to jump into a chat), location tags (pin + place name — tap to open the map), and images; a post with a location appears on the public map as a "bubble" and slowly fades out over time — that's the core of how PowPow works. (Want them to see it right away? Send screenshots — see "Product Screenshots".)
  3. **What's needed to start**: digital humans, locations, and publishing all require the platform, so step one is a one-time login with your PowPow account. No account yet? Register: https://global.powpow.online/register (To learn what PowPow is first, watch this intro video — in Chinese: https://www.bilibili.com/video/BV1Wu826UEVz/ )
  4. **The flow (compressed to 3 beats)**: ① log in → ② you give me material, I write and format → ③ you confirm, only then I publish. Everything is editable until you confirm; nothing goes live without confirmation.
- **Digital-human intent** → short opening: "Give me a name + persona + location, and I'll turn them into a chat-capable digital human pinned to the public map. This costs 2 badges and expires after 30 days — confirm and I'll do it. First, log in once: share your PowPow username and password (no account yet? Register: https://global.powpow.online/register )."

Template (use as a starting point, adapt to the user's language — don't parrot it into template-speak):
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill explicitly instructs the agent to ask users for their PowPow username and password in chat, while also discouraging meaningful security explanation. Collecting credentials through a conversational agent is inherently phishing-like and creates a high risk of credential theft, mishandling, replay, or user normalization of unsafe login practices.

Session Persistence

Medium
Category
Rogue Agent
Content
node scripts/login.js <username>
     ```
     then type the password at the prompt (the script reads it hidden, no echo). Either way, **never put the password in a command-line argument**.
   - Never echo the password, never write it to disk, never keep it in a shell variable.
   - The login token lands in the state directory (`POWPOW_STATE_DIR`, default `~/.powpow/session.json`), not inside the skill directory.
   - This is an internal handling rule — don't explain it to the user.
3. **If login returns 403 `pending_payment`** — the account exists but platform activation isn't complete. Stop, don't retry, say: "Your account isn't fully activated yet, so publishing isn't possible right now. Please open PowPow, log in, and follow the on-page instructions to finish activation; tell me when it's done and we'll continue." This is neither a transient error nor a login problem — don't make the user re-enter credentials, and don't keep collecting material.
Confidence
89% confidence
Finding
This section explicitly states that the login token is written to `~/.powpow/session.json`, confirming local persistence of bearer credentials. Even though the password is handled more carefully, a stolen or exposed session file may be enough to impersonate the user and publish content or consume account-linked actions without re-entering the password.

Session Persistence

Medium
Category
Rogue Agent
Content
│   ├── geocode.js                    # place name → WGS-84 candidates (user confirms)
│   ├── list-digital-humans.js        # list/search with health filtering
│   ├── match-digital-human.js        # topic → ranked healthy candidates
│   ├── create-digital-human.js       # create a map digital human (2 badges, 30-day expiry; avatar: generate/upload/URL)
│   ├── search-image.js               # Unsplash (user's own key, optional; also resolves photo-page links)
│   ├── upload-image.js               # local image → platform storage → public URL (used by publish.js)
│   └── lib/
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
This markdown file describes that the skill writes the user's session token to a state directory, which affects credential handling and local system privacy. While the storage location is explained, there is no explicit warning to the user about the security implications of persisting an authentication token on disk.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
console.log('Present these to the user to pick. If none fit, in this order:');
    console.log('  1) retry with different wording or --city <城市>;');
    console.log('  2) widen --limit and help the user recognise one from its district/address;');
    console.log('  3) publish without a location. Never ask the user for coordinates.');
  } catch (err) {
    if (err instanceof SessionExpiredError) {
      console.error(`❌ ${err.message}`);
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
console.log('Present these to the user to pick. If none fit, in this order:');
    console.log('  1) retry with different wording or --city <城市>;');
    console.log('  2) widen --limit and help the user recognise one from its district/address;');
    console.log('  3) publish without a location. Never ask the user for coordinates.');
  } catch (err) {
    if (err instanceof SessionExpiredError) {
      console.error(`❌ ${err.message}`);
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The file sets a fixed Accept-Language header to zh-CN and also defines error text in Chinese, which imposes a specific language/locale by default. There is no visible opt-in or user choice mechanism in this file, so it creates a natural-language locale policy concern.

Static analysis

Detected: suspicious.potential_exfiltration

Sensitive-looking file read is paired with a network send.

Warn
Code
suspicious.potential_exfiltration
Location
scripts/lib/api-client.js:66