Back to skill

Security audit

Paragraph Test

Security checks for vulnerabilities and agentic risk

Overview

This Paragraph publishing skill is mostly purpose-aligned, but it exposes high-impact publishing, subscriber, credential, and local-file upload behavior without enough guardrails.

Review carefully before installing. Use only a narrowly scoped Paragraph API key, leave PARAGRAPH_API_BASE_URL unset unless testing with non-production credentials, require human confirmation before any post creation or subscriber action, set sendWelcomeEmail to false unless outreach is intended, and only import CSV files from a user-approved path after confirming their contents.

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
skill.js:9
Finding

Configurable API Base URL Enables Bearer Credential and Sensitive Data Disclosure

Content
View full analysis
{ 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 const timeoutId = setTimeout(() => controller.abort(), timeoutMs) try { const response = await fetch(url.toString(), { method, headers, body: fetchBody, signal: controller.signal }) ``` The same configurable destination is used for subscriber imports: ```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", hea ...[truncated 2481 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
skill.js:440
Finding

Unrestricted Local File Read and Upload Through Subscriber CSV Import

Content
View full analysis
{ 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` argument is caller-controlled and passed directly to `fs.readFileSync()`. The Skill does not verify that the path: - Belongs to an approved import directory. - Resolves to a regular file. - Is not a symbolic link. - Has a `.csv` extension. - Contains CSV-formatted data. - Is within the documented 10 MB limit. - Was explicitly selected or approved by the user. Any file readable by the Skill process can therefore be loaded into memory and submitted to the configured API destination under the fixed filename `subscribers.csv`. Renaming the multipart upload does not change or sanitize its contents. The use of synchronous, unbounded file loading also blocks the Node.js event loop and can exhaust process memory when a large file or special filesystem object is supplied. ### Attack Path 1. An attacker influences an agent prompt, tool argument, automated workflow, or other source that controls `csvPath`. 2. The attacker supplies the path of a sensitive readable file ...[truncated 1197 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
skill.js:558
Finding

Public Feed Operations Unnecessarily Transmit the API Bearer Token

Content
View full analysis
{ const params = { limit } if (cursor) params.cursor = cursor const result = await request("GET", "/v1/posts/feed", null, params) return { posts: result.items || [], pagination: result.pagination || {} } }), ``` However, the shared request helper requires and attaches the bearer token to every request: ```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}` } ``` ### Technical Analysis `paragraph_getFeed` invokes the authenticated `request()` helper even though its own comment states that the endpoint is public and requires no authentication. This forces callers to configure a privileged API key and transmits that key for an operation that does not need it. This violates least-privilege credential handling. It also increases the number of requests and code paths through which the key may be exposed. The risk is amplified by the unrestricted `PARAGRAPH_API_BASE_URL`, because invoking a nominally public operation can disclose the bearer credential to a configured third-party ...[truncated 1235 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (15)

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · README.md (reported line 29)May include surrounding context.

md
## 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+)

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · README.md (reported line 81)May include surrounding context.

Agent Prompt Examples

Create a blog post from research findings

text
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

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The create-post examples encourage publishing from agent prompts without prominently stating that paragraph_createPost publishes immediately. This can cause an agent or user to treat the action as a draft/save step and accidentally post sensitive, incorrect, or unreviewed content to a public onchain platform.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The subscriber import/add examples normalize handling of email and wallet data, and may trigger welcome emails, without warning about consent, privacy, or notification side effects. In an agent context this can lead to unauthorized processing of personal data or accidental outreach to contacts, causing privacy violations, spam, or compliance issues.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The README describes a draft/review/update workflow using published: false and paragraph_updatePost, but elsewhere clearly states posts are published immediately and updates are unsupported. In an agent skill, this mismatch is dangerous because operators may rely on a nonexistent safety gate and unintentionally publish unreviewed or sensitive content publicly and onchain.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · README.md (reported line 473)May include surrounding context.

md
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

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The documentation states that posts are published onchain immediately, but it does not give a sufficiently prominent warning that blockchain publication is effectively permanent, public, and difficult or impossible to reverse. This can lead users or autonomous agents to publish sensitive, copyrighted, regulated, or erroneous content that cannot be fully retracted once committed.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill advertises subscriber addition, tagging, counting, and CSV bulk import of emails and wallet addresses without a prominent privacy, consent, retention, or misuse warning. Because these features handle personal data and audience lists, an agent or operator could import, segment, or contact people without adequate consent or awareness of compliance obligations, increasing risk of privacy violations and spam abuse.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

This tool transmits subscriber contact data to the remote API and may trigger a welcome email by default. In agent workflows, sending personally identifiable information and initiating outbound communication without an explicit warning or opt-in can lead to privacy, consent, and reputational issues even if the API call is expected behavior.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The subscriber import tool accepts an arbitrary local file path, reads that file from disk, and transmits its contents to the remote Paragraph API. In an agent setting, this creates a file-exfiltration primitive that is broader than what is necessary for a typical API client, because a prompt or tool caller could coerce the skill into uploading sensitive local files instead of an intended CSV export.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The import flow reads a local CSV from an arbitrary path and uploads it to an external service without any built-in disclosure, warning, or confirmation mechanism. In an LLM-agent context, this materially increases the risk of unintended sensitive-data transmission, since the caller may not realize that local filesystem contents are being sent off-host.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The configuration note says PARAGRAPH_PUBLICATION_SLUG is required and 'the skill will not auto-discover the slug,' but the auto-discovery section says the skill automatically discovers PARAGRAPH_PUBLICATION_ID from the API key by fetching the public feed. These statements create intent/documentation divergence about what identity metadata is discovered automatically and what the user must configure.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The prompt example tells the agent to publish content from memory/2026-03-10.md, implicitly endorsing use of a fixed internal memory source without an explicit user approval step. In agent systems, this can normalize exfiltration of stored notes or internal data into a public post, especially when combined with immediate publishing behavior.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
99% confidence
Finding

The parameter documentation and nearby comments state that waitForProcessing defaults to false, but line L272 says 'If waitForProcessing is true (default)' which is the opposite of the actual default assignment. This creates a direct contradiction in tool behavior documentation.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
97% confidence
Finding

The comment says the feed endpoint is public and requires no auth, but paragraph_getFeed calls request(), and request() always requires PARAGRAPH_API_KEY and sends an Authorization header. This is an active documentation contradiction about whether authentication is needed.

Content

No source excerpt is available for this finding.

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