Back to skill

Security audit

Clawlet

Security checks for vulnerabilities and agentic risk

Overview

This Nostr skill is mostly purpose-aligned, but it needs review because it stores and exports account-controlling private keys in plaintext and falls back to a shared default identity when user context is missing.

Review carefully before installing. Use only throwaway or low-value Nostr identities unless the skill is updated to encrypt keys or use a keychain, require authenticated user context, add confirmations for key export and network-visible actions, verify relay events, and refresh the dependency lockfile. Treat anything posted or followed through this skill as relay-distributed public activity, and treat DMs as encrypted content with observable metadata.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
index.js:42
Finding
Plaintext Storage of Nostr Private Keys with Unrestricted Default Permissions<![CDATA[ ## Vulnerability Details **File Location**: `index.js:42-47` and `index.js:67-76` **Vulnerability Type**: Plaintext sensitive-data storage **Risk Level**: High ### Vulnerable Code ```js function saveIdentities(identities) { ensureDataDir(); fs.writeFileSync(IDENTITIES_FILE, JSON.stringify(identities, null, 2)); } ``` ```js // 存储私钥(hex格式) const skHex = Buffer.from(sk).toString('hex'); identities[userId] = { privateKey: skHex, publicKey: pk, npub: npub, name: name || `user_${userId.slice(0, 8)}`, createdAt: new Date().toISOString() }; saveIdentities(identities); ``` ### Technical Analysis The Skill stores every user's Nostr private key as an unencrypted hexadecimal string in `data/identities.json`. The same file also contains identity metadata, interests, and nickname mappings. `fs.writeFileSync` is invoked without an explicit restrictive file mode. The data directory is likewise created without an explicit mode. Consequently, access is governed by the process umask and existing filesystem permissions, which may allow other local users or processes to read the file. The implementation does not validate file ownership or permissions before loading existing data. Because a Nostr private key directly controls an identity, this is equivalent to storing an authentication credential in plaintext. ### Attack Path 1. A local attacker, compromised dependency, co-hosted service, backup process, or other process gains read access to the Skill directory. 2. The attacker reads `data/identities.json`. 3. The attacker extracts the hexadecimal `privateKey` value for one or more users. 4. The attacker imports the key into another Nostr client or uses `nostr-tools` to sign arbitrary events. 5. The attacker impersonates the victim and may decrypt applicable NIP-04 direct messages. ### Impact Assessment Disclosure of a private key provides permanent control over the associated Nostr identity. The attacker can: - Publish signed posts ...[truncated 442 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store private keys in an operating-system keychain, hardware-backed keystore, or dedicated secrets manager. 2. If file-based storage is unavoidable, encrypt each private key using an authenticated encryption scheme and keep the encryption key outside the project directory. 3. Create the data directory with mode `0700` and identity files with mode `0600`. 4. Verify file ownership and permissions before reading an existing identity file; refuse operation if they are unsafe. 5. Use atomic writes through a securely created temporary file followed by a rename. 6. Separate each user's secrets rather than placing all users' private keys in one JSON document. 7. Avoid unnecessary serialization or logging of private-key material. 8. Document key rotation and incident-response procedures for users whose local identity file may have been exposed. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
index.js:542
Finding
Missing Authentication Context Falls Back to a Shared Privileged Identity<![CDATA[ ## Vulnerability Details **File Location**: `index.js:542-557`; the same fallback pattern appears throughout the exported user-scoped handlers **Vulnerability Type**: Authentication failure and cross-session identity confusion **Risk Level**: High ### Vulnerable Code ```js // 导出私钥(供用户备份) clawlet_identity_export: async (params, context) => { const userId = context?.user?.id || 'default'; const identity = getIdentity(userId); if (!identity) { return { success: false, message: '你还没有 Nostr 身份' }; } return { success: true, privateKey: identity.privateKey, npub: identity.npub, warning: '请妥善保管私钥,不要泄露给他人' }; }, ``` Equivalent fallback logic is used by identity creation and retrieval, posting, timeline access, interest management, direct messaging, and nickname management: ```js const userId = context?.user?.id || 'default'; ``` ### Technical Analysis User isolation depends entirely on `context.user.id`, but the implementation does not require that authenticated identity information be present. If the context or user ID is absent, every caller is mapped to the literal identifier `default`. This converts an authentication failure into access to a shared account. Because the shared identity can be created, exported, used to publish signed events, and used to decrypt direct messages, the fallback crosses a security boundary rather than merely selecting harmless default configuration. The export function compounds the problem by returning the raw private key without re-authentication, explicit authorization checks, or confirmation of a trusted output channel. ### Attack Path 1. A legitimate or contextless caller creates a Nostr identity while `context.user.id` is unavailable, causing it to be stored under `default`. 2. A second caller invokes a Skill function without a populated authenticated context. 3. The second caller is also resolved to the `default` ide ...[truncated 843 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Fail closed when `context.user.id` is missing, empty, or not established by a trusted authentication layer. 2. Replace the fallback with centralized validation, for example: ```js function requireAuthenticatedUser(context) { const userId = context?.user?.id; if (typeof userId !== 'string' || userId.trim() === '') { throw new Error('Authenticated user context is required'); } return userId; } ``` 3. Apply the validation consistently to every user-scoped exported handler. 4. Ensure the host, not model-controlled parameters, establishes the authenticated user ID. 5. Require explicit confirmation and recent re-authentication before exporting private keys. 6. Restrict key export to trusted, private output channels and consider disabling raw export by default. 7. Add tests proving that missing, null, empty, or malformed contexts cannot access or create an identity. 8. Add cross-user isolation tests for posts, private messages, interests, and nickname data. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
index.js:253
Finding
Unverified Events from Nostr Relays Are Treated as Trusted Data<![CDATA[ ## Vulnerability Details **File Location**: `index.js:253-258`, with downstream use at `index.js:341`, `index.js:681-704`, and `index.js:798-815` **Vulnerability Type**: Missing cryptographic verification of untrusted network events **Risk Level**: Medium ### Vulnerable Code ```js ws.on('message', (data) => { try { const msg = JSON.parse(data.toString()); if (msg[0] === 'EVENT' && msg[1] === subId) { events.push(msg[2]); } else if (msg[0] === 'EOSE' && msg[1] === subId) { if (!resolved) { resolved = true; completed++; ws.close(); } } } catch (e) {} }); ``` The returned event is subsequently consumed without calling `verifyEvent`. For example, profile data is parsed directly: ```js const profile = JSON.parse(events[0].content); ``` Direct-message events are also processed using attacker-supplied event fields: ```js for (const event of events) { try { // 获取发件人公钥 const senderPubkey = event.pubkey; // 解密内容 const decryptedContent = nip04Decrypt(sk, senderPubkey, event.content); dms.push({ id: event.id, from: senderPubkey, fromNpub: npubEncode(senderPubkey), content: decryptedContent, created_at: new Date(event.created_at * 1000).toISOString() }); } catch (e) { // 解密失败,跳过 console.log('解密失败:', e.message); } } ``` ### Technical Analysis Nostr event integrity is based on the event ID and digital signature. A relay is a transport and storage service, not a trusted signer. The Skill verifies events that it creates locally but does not call `verifyEvent` on events received from relays. As a result, a malicious or compromised relay can return an event containing an arbitrary `pubkey`, `content`, timestamp, ID, kind, or tag set. The Skill then displays, scores, parses, or attempts to decrypt those fi ...[truncated 1556 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Call `verifyEvent(event)` before accepting any relay event. 2. Validate the complete event schema, including: - Exact field types. - Valid 64-character hexadecimal IDs and public keys. - Allowed event kinds. - Reasonable timestamps. - Properly structured tags. - Maximum content and message sizes. 3. Reapply requested filters locally. Do not assume the relay honored author, kind, tag, or limit constraints. 4. Discard events whose ID does not match the event body or whose signature is invalid. 5. Deduplicate only after successful verification. 6. Limit the number and aggregate size of events retained from each relay. 7. Treat all event content as untrusted data and clearly separate it from Agent instructions. 8. Log verification failures in a rate-limited manner without logging private or decrypted message content. ]]>

T08 · Insecure Dependencies

Warning
Location
package-lock.json:19
Finding
Cryptographic Dependency Tree Is Locked to a Third-Party npm Registry Mirror<![CDATA[ ## Vulnerability Details **File Location**: `package-lock.json:19-168` **Vulnerability Type**: Unsafe dependency source and supply-chain trust **Risk Level**: Medium ### Vulnerable Code The lockfile resolves security-sensitive dependencies through `registry.npmmirror.com`, including: ```json "version": "2.1.1", "resolved": "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-2.1.1.tgz" ``` ```json "version": "2.0.1", "resolved": "https://registry.npmmirror.com/@noble/curves/-/curves-2.0.1.tgz" ``` ```json "version": "2.0.1", "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-2.0.1.tgz" ``` ```json "version": "2.23.1", "resolved": "https://registry.npmmirror.com/nostr-tools/-/nostr-tools-2.23.1.tgz" ``` ```json "version": "0.1.0", "resolved": "https://registry.npmmirror.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz" ``` ```json "version": "8.19.0", "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz" ``` ### Technical Analysis The lockfile directs installation of the complete dependency tree through a non-default registry mirror. This includes packages responsible for cryptography, key generation, event signing, encryption, and network communication. Package-lock integrity hashes provide protection against content that differs from the committed lockfile, but they do not eliminate the need to trust the lockfile's provenance or the source from which it was generated. A maliciously modified lockfile and matching integrity hash could direct installation to an altered package. A compromised dependency would execute in the same Node.js process and therefore have access to plaintext private keys. No evidence establishes that the listed mirror or packages are currently malicious. The risk arises from the unnecessary additional supply-chain trust boundary around highly sensitive cryptographic operations. ### Attack Path 1. The third-party mirror, a package published through it, or the committed lockfile is compromised. 2. A ...[truncated 1004 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Regenerate the lockfile using the official npm registry at `https://registry.npmjs.org/`. 2. Use `npm ci` in deployment so installation exactly follows the reviewed lockfile. 3. Pin reviewed dependency versions rather than relying solely on broad compatible ranges. 4. Retain and review package integrity hashes. 5. Add automated dependency provenance, vulnerability, and license scanning. 6. Review transitive cryptographic dependencies when updating `nostr-tools`. 7. Protect lockfile changes with mandatory code review and CI checks that reject unexpected registry hosts. 8. Where supported, verify package provenance attestations and signatures. 9. Run the Skill under a minimally privileged operating-system account with restricted filesystem and network access to reduce the impact of dependency compromise. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (21)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill description presents normal Nostr assistant features, but the file also indicates that users can export private keys and that private keys are stored locally in data/identities.json. Handling and exporting long-term private keys is highly sensitive; if users are not clearly warned up front, they may invoke the skill without understanding that account-compromising secret material will be stored and can be revealed.

Missing User Warnings

High
Confidence
99% confidence
Finding
Private keys are stored in a local JSON file in plaintext, which is a sensitive-secret storage weakness. Any local compromise, backup leakage, container escape, shared-host access, or accidental file exposure would immediately disclose all user identities and enable account takeover and DM decryption.

Description-Behavior Mismatch

High
Confidence
97% confidence
Finding
The skill exposes a direct private-key export capability even though the described skill purpose does not clearly establish key backup/export as a required operation. In this context, the private key is the user's full Nostr identity; any caller able to invoke this action can permanently impersonate the user, decrypt future/archived DMs available to that key, and take over the account.

Missing User Warnings

High
Confidence
95% confidence
Finding
Although the DM content is encrypted, sending private messages to third-party relays still exposes metadata such as sender, recipient, timing, and event IDs, and places sensitive communication onto external infrastructure. Without explicit warning/consent in the action path, users may wrongly assume stronger privacy guarantees than Nostr relays actually provide.

Known Vulnerable Dependency: ws==8.19.0 — 2 advisory(ies): CVE-2026-45736 (ws: Uninitialized memory disclosure); CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
98% confidence
Finding
The lockfile pins ws to 8.19.0, and the supplied finding identifies known advisories for that exact version involving memory disclosure and memory-exhaustion denial of service. In this skill, WebSocket handling is central to Nostr relay communication, so an exposed vulnerable ws client/library increases risk because the skill processes untrusted network traffic from external relays.

Known Vulnerable Dependency: ws==8.19.0 — 2 advisory(ies): CVE-2026-45736 (ws: Uninitialized memory disclosure); CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
98% confidence
Finding
The skill depends on ws 8.19.0, which is flagged with advisories for uninitialized memory disclosure and memory-exhaustion denial of service. In the context of a Nostr client/agent that communicates over WebSockets and processes untrusted remote data, these issues are more dangerous because an attacker-controlled relay or peer could potentially trigger information leakage or exhaust process memory.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The README advertises generation and management of Nostr keys but provides no warning that these are highly sensitive credentials whose disclosure would allow account takeover and impersonation. In an agent skill context, users may ask the assistant to create or handle keys conversationally, increasing the chance that private keys are exposed in logs, chat history, screenshots, or insecure storage.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The README describes sending content to the Nostr network without warning that posts may be public, replicated across relays, and difficult or impossible to retract. In this skill's context, users may treat the agent like a private assistant and unknowingly instruct it to publish sensitive, personal, or regulated information to a public network.

Lp3

Medium
Category
MCP Least Privilege
Confidence
79% confidence
Finding
The skill performs sensitive operations around Nostr identity management and private-key handling, yet the manifest does not declare an explicit tool scope or permissions boundary. Missing scope makes it harder for users and the host to understand what resources the skill may access, increasing the risk of overbroad execution or unintended access to environment-backed secrets.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The skill advertises posting and private messaging features but does not clearly warn in the main description that content will be transmitted to external relays. In this context, omission is more dangerous because Nostr posts and relay-delivered messages can be widely replicated or metadata-exposed, and users may not realize they are sending data off-device to third-party infrastructure.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The trigger examples include broad everyday phrases like '有什么新消息', which can overlap with ordinary conversation and unintentionally activate the skill. Because this skill can post content, manage identities, and send direct messages, accidental invocation could lead to unintended external actions or disclosure of social-graph and message data.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
User-facing strings throughout the skill are consistently in Chinese, and the header brands the tool in Chinese, but there is no opt-in language selection or explanation that the skill is intended only for a Chinese-language audience. This can violate a language/locale policy requiring user choice or explicit locale justification.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This function publishes user-provided content to multiple external WebSocket relays, which is a network transmission of user data. The code does not show a confirmation prompt, visible log/print, or warning in this path that the content will be sent to third-party relays.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
Following a user creates and publishes a social graph event to external relays. There is no prompt, visible logging, or explanatory warning in this action path informing the user that the follow event will be broadcast publicly to third-party servers.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The manifest says the skill can read a timeline, which normally implies the user's Nostr timeline or feed. The implementation fetches recent kind-1 events from relays without using the user's follow list, so it behaves like a global public feed reader rather than a user-specific timeline.

Missing User Warnings

Low
Confidence
78% confidence
Finding
The relay fetch helper opens WebSocket connections to external Nostr relays and sends query filters, which is network activity involving user-driven lookups. There is no visible prompt, log, or explanatory comment indicating to the user that external relay access occurs for reads as well as writes.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The inline documentation describes '发送私信并等待回复(模拟对话)', implying conversational reply handling. In reality, the function just delegates to the send operation and returns immediately with a note to use another function to view replies, which contradicts the stated intent of waiting for a response.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"author": "",
  "license": "ISC",
  "dependencies": {
    "https-proxy-agent": "^7.0.6",
    "nostr-tools": "^2.23.1",
    "ws": "^8.19.0"
  }
Confidence
88% confidence
Finding
The dependency uses a caret range, which permits automatic installation of newer minor/patch releases instead of a fully fixed version. This weakens supply-chain reproducibility and can unexpectedly introduce vulnerable or malicious upstream changes, though by itself it is a lower-severity hygiene issue.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "ISC",
  "dependencies": {
    "https-proxy-agent": "^7.0.6",
    "nostr-tools": "^2.23.1",
    "ws": "^8.19.0"
  }
}
Confidence
88% confidence
Finding
The nostr-tools dependency is specified with a caret range, allowing non-identical builds over time as upstream minor/patch releases are pulled in. For a skill that manages identities, timelines, and private messaging, this increases supply-chain risk because cryptographic or protocol-handling behavior could change without review.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "https-proxy-agent": "^7.0.6",
    "nostr-tools": "^2.23.1",
    "ws": "^8.19.0"
  }
}
Confidence
93% confidence
Finding
The ws package is not only unpinned via a caret range but is also the WebSocket library likely exposed to untrusted network traffic in this Nostr skill. Unpinned network-facing dependencies raise supply-chain risk and make it easier for insecure or unexpected upstream changes to reach production unnoticed.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
The manifest description is written entirely in Chinese and presents the skill as a general-purpose "smart assistant" for Nostr, with no indication that the skill is China-specific or that users can choose another language. This can conflict with language/locale policy expectations when a skill is not explicitly scoped to a Chinese-speaking audience.

Static analysis

No suspicious patterns detected.