Back to skill

Security audit

Paragraph

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it claims, but it has review-worthy risks around public publishing, subscriber data uploads, and sending credentials to a configurable API host.

Install only if you are comfortable giving the skill a Paragraph API key that can publish posts and manage subscribers. Keep PARAGRAPH_API_BASE_URL unset or pinned to the official Paragraph API, review all content before invoking post creation, and only import subscriber CSVs from intentional, verified paths with proper consent for email or wallet-address processing.

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

T09 · Insecure Skill Coding Practices

Error
Location
skill.js:9
Finding
Paragraph Credentials and Sensitive Request Data Can Be Redirected to an Arbitrary Host<![CDATA[ ## Vulnerability Details **File Location**: `skill.js:9-75` **Vulnerability Type**: Unrestricted credential-bearing network destination **Risk Level**: High ### Vulnerable Code ```javascript const API_BASE = process.env.PARAGRAPH_API_BASE_URL || "https://public.api.paragraph.com/api" ``` ```javascript async function request(method, endpoint, body = null, params = {}, options = {}) { // Read API_KEY from env at call time to respect per-skill injection const apiKey = process.env.PARAGRAPH_API_KEY if (!apiKey) { throw new Error("PARAGRAPH_API_KEY environment variable not set") } const url = new URL(`${API_BASE}${endpoint}`) Object.keys(params).forEach(key => { if (params[key] !== undefined && params[key] !== null) { url.searchParams.append(key, String(params[key])) } }) const headers = { "Authorization": `Bearer ${apiKey}` } let fetchBody = null if (body) { headers["Content-Type"] = "application/json" fetchBody = JSON.stringify(body) } if (options.rawBody) { fetchBody = options.rawBody Object.assign(headers, options.headers) } else if (options.formData) { fetchBody = options.formData // Don't set Content-Type; fetch will set boundary } // Set up abort controller for timeout const controller = new AbortController() const timeoutMs = options.timeout || 30000 // default 30 seconds (POSTs can be slow) const timeoutId = setTimeout(() => controller.abort(), timeoutMs) try { const response = await fetch(url.toString(), { method, headers, body: fetchBody, signal: controller.signal }) ``` The CSV import path independently uses the same configurable destination: ```javascript const url = new URL(`${API_BASE}/v1/subscribers/import`) url.searchParams.append('sendWelcomeEmail', sendWelcomeEmail) const formData = new FormData() formData.append('file', csvBuffer, 'subscribers.csv') const response = await fetch(url.toString(), { method: "PO ...[truncated 2802 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the production base-URL override unless it is operationally essential. 2. For production use, hard-code and validate the destination: ```javascript const OFFICIAL_API_ORIGIN = "https://public.api.paragraph.com" const API_BASE = `${OFFICIAL_API_ORIGIN}/api` ``` 3. If test endpoints must remain supported: - Require an explicit development/test-mode flag. - Maintain an allowlist of approved HTTPS origins. - Reject URLs containing credentials, non-HTTPS protocols, unexpected ports, or unapproved hosts. - Use separate test credentials rather than the production API key. 4. Before attaching `Authorization`, compare `url.origin` to the expected credential origin: ```javascript if (url.origin !== "https://public.api.paragraph.com") { throw new Error("Refusing to send Paragraph credentials to an unapproved origin") } ``` 5. Apply the same centralized request validation to CSV imports rather than constructing a second direct `fetch` request. 6. Document the exact network destination and credential scope, and encourage users to create narrowly scoped, revocable API keys. 7. Add tests proving that HTTP URLs, alternate domains, user-info URLs, and unapproved ports are rejected before any network request occurs. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
skill.js:439
Finding
Subscriber Import Permits Unrestricted Local File Reads and Network Uploads<![CDATA[ ## Vulnerability Details **File Location**: `skill.js:439-459` **Vulnerability Type**: Arbitrary local file read through an agent-controlled path **Risk Level**: High ### Vulnerable Code ```javascript paragraph_importSubscribers: wrapTool(async ({ csvPath, sendWelcomeEmail = true }) => { if (!csvPath) throw new Error("csvPath is required") const fs = await import('fs') const csvBuffer = fs.readFileSync(csvPath) // Build URL with query param const url = new URL(`${API_BASE}/v1/subscribers/import`) url.searchParams.append('sendWelcomeEmail', sendWelcomeEmail) const formData = new FormData() formData.append('file', csvBuffer, 'subscribers.csv') const response = await fetch(url.toString(), { method: "POST", headers: { "Authorization": `Bearer ${process.env.PARAGRAPH_API_KEY}` // Content-type (with boundary) set automatically by fetch when using FormData }, body: formData }) ``` ### Technical Analysis The `csvPath` tool argument is passed directly to `fs.readFileSync()`. The Skill performs no validation that the resolved target: - Is inside an approved import or workspace directory. - Is a regular file. - Has a CSV extension or valid subscriber CSV structure. - Is below the documented maximum size. - Is not reached through path traversal or a symbolic link. - Was explicitly selected and confirmed by the user. Consequently, the operation inherits all filesystem-read permissions of the OpenClaw process rather than limiting access to files necessary for subscriber import. The entire file is loaded synchronously into memory and then placed into a multipart network request. This is especially dangerous in an agent context because untrusted instructions can attempt to induce a tool call with a sensitive absolute path or traversal path. When combined with the unrestricted API destination vulnerability, the file is delivered directly to an attacker-controlled server. Even with the legitimate Paragraph desti ...[truncated 1998 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict imports to a dedicated directory controlled by the application, such as an approved workspace import folder. 2. Canonicalize both the allowed root and requested file using `realpath`, then verify that the resolved file remains beneath the allowed root: ```javascript const root = await fs.promises.realpath(APPROVED_IMPORT_DIRECTORY) const target = await fs.promises.realpath(csvPath) const relative = path.relative(root, target) if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("CSV file must be inside the approved import directory") } ``` 3. Use `lstat`/`stat` to require a regular file and adopt an explicit policy for rejecting symbolic links. 4. Enforce a conservative maximum size before reading or uploading the file. 5. Validate the extension, MIME expectation, CSV header row, row count, and field structure before transmission. Content validation is supplementary and must not replace path containment. 6. Prefer a bounded streaming upload over `readFileSync()` to avoid blocking and unbounded memory consumption. 7. Require explicit user confirmation that displays: - The canonical local path. - The resolved destination origin. - The file size. - Whether welcome emails will be sent. 8. Set `sendWelcomeEmail` to `false` by default for bulk imports to reduce unintended external side effects. 9. Route the upload through the centralized authenticated request helper after adding strict destination validation and timeouts. 10. Add tests for absolute sensitive paths, `../` traversal, symlink escapes, oversized files, non-regular files, malformed CSV data, and unapproved network destinations. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (11)

Session Persistence

Medium
Category
Rogue Agent
Content
## Prerequisites

1. **Paragraph Account**: Create an account at [paragraph.com](https://paragraph.com)
2. **API Key**: Generate one in Account Settings → Integrations
3. **Node.js 19+**: Required for native fetch (OpenClaw uses Node 24+)
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.

Session Persistence

Medium
Category
Rogue Agent
Content
### Agent Prompt Examples

**Create a blog post from research findings**
```
You are a content creator. Use the paragraph skill to publish a new blog post
based on today's research notes. Title: "AI Trends 2026", markdown content
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
96% confidence
Finding
The example prompt encourages creating a post from research notes without a clear warning that the tool publishes immediately and may create an irreversible onchain record. For agent-driven workflows, omission of that warning is risky because users may treat the example like a safe drafting action and accidentally publish sensitive, inaccurate, or unreviewed content.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The subscriber add/import flows process personal data such as email addresses and wallet addresses, and may also trigger welcome emails, but the README does not prominently warn about privacy, consent, or transmission to a third-party service. In an agent context, this increases the chance that a user bulk-imports contacts without understanding the privacy and notification consequences.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The README gives workflow guidance that contradicts the rest of the document: it recommends draft/update publishing using `published: false` and `paragraph_updatePost`, while elsewhere it explicitly says updates are unsupported and posts publish immediately onchain. In an agent skill, misleading operational docs are dangerous because they can cause users or higher-level agents to assume review gates exist when the actual action is immediate and potentially irreversible.

Session Persistence

Medium
Category
Rogue Agent
Content
1. Writer generates markdown draft
2. Publisher Agent calls `paragraph_createPost` with `published: false`
3. Review/approval step
4. Publish: `paragraph_updatePost` with `published: true` OR create with `published: true`
5. If tokenizing: capture coin ID from response, track via `paragraph_getCoin` and `paragraph_listCoinHolders`
6. Analytics: correlate engagement (views, holders) with content performance
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
92% confidence
Finding
The skill states that posts are published onchain immediately upon creation, but it does not elevate the consequence that such publication may be effectively irreversible, publicly permanent, and difficult to correct after submission. In an automation context, this omission can cause accidental disclosure of sensitive, confidential, or legally risky content through a single mistaken tool call.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill explicitly supports adding subscribers, tagging them, and importing bulk CSVs containing email addresses and wallet-linked audience data, but it does not present a clear privacy/compliance warning where those features are described. This creates a real risk of misuse, unauthorized contact import, or mishandling regulated personal data, especially because wallet addresses can be linkable identifiers in a Web3 context.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The import tool reads an arbitrary local path from `csvPath` and uploads its contents to a remote API using the skill's credentials, with no path restrictions, validation, or explicit confirmation gate. In an agent setting, this increases the risk of unintended local file exfiltration if an upstream prompt, tool caller, or compromised workflow supplies a sensitive file path instead of a subscriber CSV.

Intent-Code Divergence

Low
Confidence
90% confidence
Finding
The troubleshooting section says `tags` must be an array of strings, but the documented `paragraph_createPost` parameters define `categories` as the tag field. This is contradictory guidance that can mislead users about what the skill actually accepts.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The documentation states that `waitForProcessing` defaults to false, but the inline comment inside the function also claims the subsequent polling branch is 'default' and says 'If waitForProcessing is true (default)'. The implementation clearly sets `waitForProcessing = false`, so the comment at L272 misrepresents what the code actually does.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
skill.js:9

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
SKILL.md:222