Back to skill

Security audit

Paragraph for OpenClaw

Security checks for vulnerabilities and agentic risk

Overview

This Paragraph publishing skill is mostly purpose-aligned, but it has review-worthy risks around credential-bearing API redirection, arbitrary local CSV upload, and potentially irreversible public publishing/subscriber actions.

Install only if you trust the environment configuration and will keep PARAGRAPH_API_BASE_URL fixed to the official Paragraph API. Treat post creation, sendNewsletter, sendWelcomeEmail, and subscriber import as actions requiring explicit user approval. Only pass a verified subscriber CSV path, and avoid using this skill with sensitive local files accessible to the agent process.

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
Configurable API Base URL Can Exfiltrate API Credentials and Sensitive Data<![CDATA[ ## Vulnerability Details **File Location**: `skill.js:9`, `skill.js:35-76`, and `skill.js:447-459` **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 } const controller = new AbortController() const timeoutMs = options.timeout || 30000 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: "POST", headers: { "Authorization": `Bea ...[truncated 2857 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `PARAGRAPH_API_BASE_URL` from production configuration and use the fixed official origin: ```javascript const API_BASE = "https://public.api.paragraph.com/api" ``` 2. If endpoint overriding is essential for development, enforce an explicit allowlist: ```javascript const allowedOrigins = new Set([ "https://public.api.paragraph.com" ]) const baseUrl = new URL( process.env.PARAGRAPH_API_BASE_URL || "https://public.api.paragraph.com/api" ) if (!allowedOrigins.has(baseUrl.origin)) { throw new Error("Unapproved Paragraph API origin") } ``` 3. Reject non-HTTPS destinations, embedded URL credentials, unexpected ports, and malformed URLs. 4. Prevent cross-origin redirect credential leakage. Use a restrictive redirect policy such as `redirect: "error"`, or manually verify the destination origin before following a redirect. 5. Separate production and test modes. Test mode should require: - An explicit opt-in flag. - Dedicated non-production API credentials. - A narrowly defined test-host allowlist. - A safeguard preventing production-format credentials from being used with test hosts. 6. Validate the final URL immediately before each request rather than only validating configuration during module initialization. 7. Clearly document which data each tool sends externally and require user confirmation for especially sensitive operations such as subscriber imports. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
skill.js:438
Finding
Subscriber CSV Import Can Read and Upload Arbitrary Process-Readable Files<![CDATA[ ## Vulnerability Details **File Location**: `skill.js:438-459` **Vulnerability Type**: Unrestricted local file read followed by network upload **Risk Level**: Medium ### 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 import tool accepts an arbitrary `csvPath`, reads the target synchronously with the Skill process's filesystem permissions, and transmits the resulting bytes over the network. It does not verify that: - The path is within an approved import directory. - The path resolves to a regular file rather than a symbolic link or special file. - The selected file has a `.csv` extension. - The file is valid subscriber CSV content. - The file is below the documented 10 MB limit. - The user explicitly approved the specific file before transmission. Renaming the multipart item to `subscribers.csv` does not make the underlying content a CSV file. Any process-readable local file can be supplied and uploaded. This violates least privilege because a subscriber import operation only needs access to a specific, user-approved CSV file. It does not need unrestricted read access to arbitrary paths available to the OpenClaw process. The risk becomes more severe when combined with the configurable API destination: an attac ...[truncated 1641 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict imports to a dedicated, user-controlled directory: ```javascript import path from "node:path" import fs from "node:fs" const IMPORT_ROOT = path.resolve( process.env.PARAGRAPH_IMPORT_DIR || "./imports" ) const candidate = path.resolve(IMPORT_ROOT, csvPath) const relative = path.relative(IMPORT_ROOT, candidate) if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("CSV path must remain within the approved import directory") } ``` 2. Resolve symbolic links with `realpath` and verify that the resulting canonical path remains within the approved directory. 3. Use `lstat` or `stat` to require a regular file and reject symbolic links, directories, devices, sockets, and named pipes as appropriate. 4. Require a `.csv` extension and validate the contents before upload: - Confirm expected headers such as email and wallet-address fields. - Reject binary or malformed input. - Reject content that does not match the subscriber import schema. 5. Enforce the documented maximum size before reading: ```javascript const stats = fs.statSync(candidate) const MAX_CSV_SIZE = 10 * 1024 * 1024 if (!stats.isFile() || stats.size > MAX_CSV_SIZE) { throw new Error("CSV must be a regular file no larger than 10 MB") } ``` 6. Prefer streaming the approved file instead of loading the entire file synchronously into memory. 7. Require explicit user confirmation that identifies: - The canonical local path. - The file size. - The destination origin. - Whether welcome emails will be sent. 8. Combine these controls with a fixed or allowlisted Paragraph API origin so imported data cannot be redirected to an arbitrary host. ]]>
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 (9)

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.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The subscriber-management features describe adding, listing, and importing subscribers, including email addresses and wallet addresses, without a clear privacy, consent, or data-handling warning. In practice this can normalize bulk processing of personal data and lead to unauthorized import, exposure, or misuse of subscriber PII by agents or operators.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The README gives contradictory guidance about the publishing workflow: elsewhere it states drafts and post updates are not supported, but this section instructs agents to use `published: false` and `paragraph_updatePost`. In an automation context, that can cause operators or downstream agents to assume a safe review step exists when in reality `paragraph_createPost` publishes immediately, increasing the risk of unintended public/onchain release of content.

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
90% confidence
Finding
The documentation mentions onchain anchoring and delayed slug availability, but it does not clearly and prominently warn that creating a post is effectively an irreversible publication action with permanent or difficult-to-remove consequences. In an agentic context, this can cause accidental permanent publication of drafts, secrets, copyrighted material, or unreviewed content because users may treat createPost like a reversible draft operation.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill exposes subscriber add/import capabilities that process personal data such as email addresses and wallet identifiers, but the documentation does not prominently warn users that they are uploading and handling third-party personal data. This increases the risk of privacy violations, accidental bulk ingestion of contacts without consent, and non-compliant use in regulated contexts, especially because the skill encourages migration/import workflows.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The import tool accepts an arbitrary local filesystem path, reads that file, and transmits its contents to a remote API. In an agent setting, this is dangerous because a prompt or indirect instruction could coerce the agent into exfiltrating sensitive local files under the guise of a CSV import, especially since there is no path restriction, file-type validation, or explicit consent gate.

Missing User Warnings

Low
Confidence
81% confidence
Finding
Although the README warns that posts are published immediately, it does not equally emphasize the distribution consequences of `sendNewsletter`, which can trigger email delivery to subscribers. An agent or operator may treat post creation as a low-risk content action and unintentionally send mass communications or publish unreviewed material to a mailing list.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The inline comment says 'If waitForProcessing is true (default)', but the parameter is initialized as waitForProcessing = false at L250 and the surrounding docstring also describes false as the default. This is an active contradiction in code documentation that could mislead users about whether the tool blocks for full post processing.

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:223