Back to skill

Security audit

Agent Postcard

Security checks for vulnerabilities and agentic risk

Overview

This postcard skill does what it says, but it can automatically send persona-derived local file content to a third-party API without a clear confirmation step.

Review before installing if your agent persona files may contain private instructions, identity details, secrets, or workspace context. Prefer using --selfie with a sanitized prompt, or only use --persona with a file created specifically for postcard appearance data. Treat Turai as receiving the generated selfie prompt plus the requested location, style, and message.

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)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/send-postcard.mjs:98
Finding
Automatic Disclosure of Persona File Content to a Third-Party Service<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send-postcard.mjs:98-170` **Vulnerability Type**: Automatic sensitive-data disclosure and excessive file access **Risk Level**: Medium ### Vulnerable Code ```js const WORKSPACE = process.env.OPENCLAW_WORKSPACE || resolve(process.cwd()); async function fileExists(p) { try { await access(p); return true; } catch { return false; } } async function readPersona() { if (args.selfie) return args.selfie; // Try explicit path first, then standard locations const candidates = args.persona ? [resolve(args.persona)] : [ join(WORKSPACE, "SOUL.md"), join(WORKSPACE, "IDENTITY.md"), join(WORKSPACE, "..", "SOUL.md"), ]; for (const p of candidates) { if (await fileExists(p)) { const content = await readFile(p, "utf-8"); return extractSelfiePrompt(content); } } console.warn("Warning: No persona file found. Using generic selfie prompt."); return "A friendly AI assistant robot with a warm smile"; } function extractSelfiePrompt(personaText) { // Take the first ~500 chars of meaningful content, strip markdown headers const lines = personaText .split("\n") .filter((l) => !l.startsWith("#") && l.trim().length > 0) .slice(0, 10); const description = lines.join(" ").slice(0, 500).trim(); if (description.length < 20) { return "A friendly AI assistant with a distinctive personality"; } // Wrap it as a selfie prompt — the API will interpret this return `Based on this persona, generate a selfie of this character: ${description}`; } async function sendPostcard({ selfiePrompt, location, style, message }) { const url = "https://turai.org/api/agent/postcard"; const body = { selfiePrompt, location, style, ...(message && { message }), }; const res = await fetch(url, { method: "POST", headers: { "x-api-key": apiKey, "Content-Type": "application/json", Accept: "application/ ...[truncated 2064 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require an explicit `--selfie` or `--persona` argument rather than automatically reading persona files. 2. Remove the parent-directory fallback: ```js join(WORKSPACE, "..", "SOUL.md") ``` 3. If automatic discovery remains necessary, restrict all resolved paths to the configured workspace using canonical path checks. 4. Show the exact extracted prompt and require explicit confirmation before sending it to an external service. 5. Provide a noninteractive consent flag for trusted automation, such as `--allow-persona-upload`. 6. Parse only a dedicated, documented appearance field instead of taking the first general-purpose persona lines. 7. Apply redaction for secrets, tokens, email addresses, private instructions, and other sensitive patterns. 8. Document what persona data is transmitted, where it is sent, and the third party's retention and privacy implications. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/send-postcard.mjs:197
Finding
Unvalidated Server-Controlled Image URL Enables Blind SSRF and Resource Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send-postcard.mjs:197-205` **Vulnerability Type**: Server-Side Request Forgery and unbounded response buffering **Risk Level**: Medium ### Vulnerable Code ```js const imageUrl = result.imageUrl || result.image_url || result.url; if (imageUrl) { console.log(`⬇️ Downloading image from: ${imageUrl}`); const imgRes = await fetch(imageUrl); if (!imgRes.ok) throw new Error(`Failed to download image: ${imgRes.status}`); const buf = Buffer.from(await imgRes.arrayBuffer()); await writeFile(outPath, buf); console.log(`🖼️ Saved postcard to: ${outPath}`); return outPath; } ``` ### Technical Analysis The image URL is taken directly from the Turai API's JSON response and passed to `fetch()` without validation. The code does not restrict the URL scheme or hostname, resolve and reject private addresses, validate redirect targets, enforce an image content type, or impose a response-size limit. If the API or its response path is compromised, an attacker can return a URL targeting loopback interfaces, private network ranges, link-local services, or cloud metadata endpoints. The script then makes a request from the agent host's network context. Because the response is not returned to the URL-controlling party, this is primarily blind SSRF; however, status behavior, timing, and state-changing GET requests can still be exploitable. The use of `arrayBuffer()` buffers the complete response in memory before writing it. A hostile server can therefore return an oversized or indefinitely streamed body, causing excessive memory consumption. The resulting data is then written to disk without checking that it is an image. ### Attack Path 1. An attacker compromises or controls the API response, or otherwise causes the postcard endpoint to return attacker-selected JSON. 2. The response contains `imageUrl`, `image_url`, or `url` pointing to: - an internal HTTP service, - a loopback or link-local endpoin ...[truncated 1096 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only `https:` URLs. 2. Maintain an explicit allowlist of trusted image-delivery hostnames. 3. Resolve the hostname and reject loopback, private, link-local, multicast, reserved, and cloud metadata address ranges for both IPv4 and IPv6. 4. Disable redirects or revalidate the scheme, hostname, DNS result, and resolved address after every redirect. 5. Enforce a short connection and total request timeout. 6. Enforce a strict maximum download size using `Content-Length` when available and a streaming byte counter regardless of whether that header is present. 7. Validate that the response `Content-Type` is an expected image type, such as `image/png` or `image/jpeg`. 8. Verify image signatures before saving and reject HTML, JSON, executables, and malformed image data. 9. Stream accepted responses to a safely created file instead of buffering the complete response in memory. 10. Prefer receiving image bytes directly from the trusted postcard endpoint rather than following arbitrary response-provided URLs. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Ae1

High
Category
analysis-evasion
Content
- `SKILL.md` — This file
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The skill explains that the agent reads its own persona and sends a generated selfie prompt to the Turai Postcard API, but it does not warn users that persona content and user-provided location/message data are disclosed to a third-party service. Because persona files may contain sensitive internal instructions or identifying details, omission of this warning can lead to unintentional data exfiltration.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The agent-facing trigger is broad and unconstrained: a natural-language request to 'send me a postcard' could cause the agent to read persona files and transmit derived content plus user-supplied location/message data to an external service without an explicit confirmation or data-boundary check. In this skill's context, that increases the risk of unintended external data disclosure and autonomous invocation from casual chat instructions.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/send-postcard.mjs:69