Back to skill

Security audit

Paper Design

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Paper design-tool bridge, but it has review-worthy risks around unrestricted endpoint configuration and local file writes.

Install only if you trust this Paper workflow and understand that it can read and modify open design files. Avoid setting PAPER_MCP_URL unless it points to a trusted local Paper server, do not use --save with sensitive or existing paths, and prefer running on a single-user machine until temp-file handling is tightened.

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
paper.sh:23
Finding
Unrestricted MCP Endpoint Allows Design Data Disclosure to Remote Servers<![CDATA[ ## Vulnerability Details **File Location**: `paper.sh:23, 35-39, 70-73, 123-127` **Vulnerability Type**: Unrestricted external endpoint configuration **Risk Level**: High ### Vulnerable Code ```bash PAPER_MCP_URL="${PAPER_MCP_URL:-http://127.0.0.1:29979/mcp}" ``` ```bash check_paper() { if ! curl -s --max-time 3 -o /dev/null -w "%{http_code}" "$PAPER_MCP_URL" -X POST \ -H "Content-Type: application/json" \ -H "Accept: $ACCEPT_HEADER" \ -d '{"jsonrpc":"2.0","method":"ping","id":0}' 2>/dev/null | grep -q "200\|400\|405"; then ``` ```bash response=$(curl -s -i -X POST "$PAPER_MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: $ACCEPT_HEADER" \ -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"openclaw-paper-skill","version":"1.0.0"}},"id":1}' 2>&1) ``` ```bash response=$(curl -s -X POST "$PAPER_MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: $ACCEPT_HEADER" \ -H "Mcp-Session-Id: $session_id" \ -d "$payload" 2>&1) ``` ### Technical Analysis The skill is documented as a bridge to Paper's local MCP server, but `PAPER_MCP_URL` is accepted directly from the process environment without validating its scheme, hostname, port, or path. Every initialization request and subsequent MCP tool request is sent to that endpoint. An attacker who can influence the environment of the skill process can replace the loopback URL with a remote server. The transmitted payload can include MCP tool names, node identifiers, HTML content, text content, design metadata, or other arguments supplied during design operations. The MCP session identifier received from the configured server is also sent in later requests. Because the remote response is trusted as an MCP response, the issue additionally permits an attacker-controlled endpoint to supply arbitrary response content and image bytes to downstream response-processing functions. ### Attack Path 1 ...[truncated 1189 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `PAPER_MCP_URL` and require the hostname to be exactly `127.0.0.1`, `::1`, or another explicitly approved local endpoint. 2. Restrict the URL scheme to plain HTTP over loopback unless authenticated TLS is deliberately supported. 3. Restrict the port and path to the expected Paper MCP endpoint. 4. Disable HTTP redirects with `curl --max-redirs 0` so a local endpoint cannot redirect requests externally. 5. Reject URLs containing user information, fragments, unexpected query parameters, or alternate address representations. 6. If endpoint customization is required, use a trusted configuration file with strict ownership and permissions rather than an unrestricted inherited environment variable. 7. Authenticate the local MCP server where the protocol supports it and validate response size and structure before processing. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
paper.sh:24
Finding
Predictable Shared Temporary Session File Enables Symlink Attacks and Session Exposure<![CDATA[ ## Vulnerability Details **File Location**: `paper.sh:24, 61-64, 88-90` **Vulnerability Type**: Unsafe temporary file handling **Risk Level**: High ### Vulnerable Code ```bash PAPER_SESSION_FILE="${PAPER_SESSION:-/tmp/paper-mcp-session}" ``` ```bash if [ -f "$PAPER_SESSION_FILE" ]; then local age=$(( $(date +%s) - $(stat -f %m "$PAPER_SESSION_FILE" 2>/dev/null || echo 0) )) if [ "$age" -lt 1800 ]; then cat "$PAPER_SESSION_FILE" return 0 fi fi ``` ```bash # Cache session echo "$session_id" > "$PAPER_SESSION_FILE" echo "$session_id" ``` ### Technical Analysis The MCP session identifier is stored at a fixed, predictable path under the shared `/tmp` directory. The script reads and truncates this path without verifying that it is a regular file owned by the current user and without rejecting symbolic links. The shell redirection in `echo "$session_id" > "$PAPER_SESSION_FILE"` follows symbolic links. A local attacker able to create the predictable path before the victim runs the skill can point it to another file writable by the victim. The next session initialization then truncates and overwrites that target with the MCP session identifier. The code also does not set a restrictive `umask` or explicitly assign permissions such as mode `0600`. Depending on the user's active `umask`, the session identifier may be readable by other local users. A pre-created regular file can also contain an attacker-chosen session identifier that the script will consume when its modification time is less than 30 minutes old. ### Attack Path 1. A local attacker predicts the fixed path `/tmp/paper-mcp-session`. 2. The attacker performs one of the following: - Creates a symbolic link at that path to a file writable by the victim. - Creates a regular file containing an attacker-selected session identifier and gives it a recent modification time. - Monitors or reads an insufficiently protected cache file. 3. The victim invokes `paper.sh`. 4. If a n ...[truncated 950 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store session data in a private per-user runtime directory, such as `$XDG_RUNTIME_DIR`, after verifying that it is owned by the current user and has mode `0700`. 2. Set `umask 077` before creating any session or screenshot files. 3. Create the session file atomically with `mktemp` inside the private directory. 4. Write the session identifier to the temporary file, set mode `0600`, and atomically rename it to the final cache name. 5. Before reading an existing cache, verify with `lstat` that it is a regular file, not a symbolic link, and is owned by the effective user. 6. Open files with no-follow and exclusive-creation semantics where available. 7. Validate the cached session identifier against the expected character set and maximum length. 8. Remove stale session files securely and avoid accepting an arbitrary `PAPER_SESSION` path unless it passes the same ownership and file-type checks. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
paper.sh:149
Finding
Unrestricted Screenshot Save Path Permits Overwriting Arbitrary User-Writable Files<![CDATA[ ## Vulnerability Details **File Location**: `paper.sh:149-170, 237, 252-260` **Vulnerability Type**: Arbitrary file overwrite **Risk Level**: High ### Vulnerable Code ```bash handle_screenshot() { local json_response="$1" local save_path="${2:-}" mkdir -p "$PAPER_SCREENSHOT_DIR" if [ -z "$save_path" ]; then save_path="$PAPER_SCREENSHOT_DIR/screenshot-$(date +%s).jpg" fi # Extract base64 image data and save python3 -c " import json, base64, sys data = json.loads(sys.stdin.read()) result = data.get('result', {}) content = result.get('content', []) for item in content: if item.get('type') == 'image': img_data = base64.b64decode(item['data']) with open(sys.argv[1], 'wb') as f: f.write(img_data) print(f'Screenshot saved: {sys.argv[1]}') print(f'Size: {len(img_data)} bytes') print(f'Format: {item.get(\"mimeType\", \"unknown\")}') sys.exit(0) ``` ```bash --save) SAVE_PATH="$2"; shift 2 ;; ``` ```bash if $RAW_OUTPUT; then echo "$RESPONSE" elif [ "$TOOL_NAME" = "get_screenshot" ]; then handle_screenshot "$RESPONSE" "$SAVE_PATH" else extract_text "$RESPONSE" fi ``` ### Technical Analysis The `--save` option accepts an unrestricted filesystem path. The Python handler opens that path using mode `wb`, which creates a missing file or truncates an existing file before writing decoded response bytes. The destination is not confined to `PAPER_SCREENSHOT_DIR`. The implementation does not canonicalize the path, reject path traversal, reject symbolic links, require exclusive file creation, or verify that the destination is a regular file. It also trusts an MCP response item merely because its declared type is `image`; it does not validate the decoded bytes against the reported MIME type, enforce a size limit, or confirm that the data is a valid image. The bytes originate from the configured MCP endpoint. Consequently, if the endpoint is malicious or compromised, it contr ...[truncated 1489 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve the requested output path and require it to remain beneath a private, dedicated screenshot directory. 2. Reject absolute paths, `..` components, symbolic links, hard links, device files, FIFOs, sockets, and other non-regular destinations. 3. Create output files with exclusive creation and no-follow semantics rather than opening existing files with truncation. 4. Generate server-independent random filenames with `mktemp` and return the generated path to the caller. 5. If caller-selected filenames are required, accept only a basename matching a strict allowlist and append an approved image extension. 6. Decode into a temporary file, enforce a conservative maximum size, validate the image signature and structure, and then atomically rename it. 7. Verify that the declared MIME type matches the decoded file format. 8. Set the screenshot directory to mode `0700` and output files to mode `0600`. 9. Reject `--save` when it lacks a following value instead of accessing `$2` under ambiguous argument conditions. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (8)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill explicitly instructs the agent to invoke a local shell script (`paper.sh`) via `exec`, but the manifest does not declare any `permissions` or `allowed-tools` scope limiting shell use. That creates an over-broad execution surface: a user-invocable skill can trigger local command execution without explicit tool scoping, increasing the risk of unintended or abused shell access if the wrapper script or its arguments are ever influenced unexpectedly.

External Transmission

Medium
Category
Data Exfiltration
Content
# Check if Paper is reachable
check_paper() {
  if ! curl -s --max-time 3 -o /dev/null -w "%{http_code}" "$PAPER_MCP_URL" -X POST \
    -H "Content-Type: application/json" \
    -H "Accept: $ACCEPT_HEADER" \
    -d '{"jsonrpc":"2.0","method":"ping","id":0}' 2>/dev/null | grep -q "200\|400\|405"; then
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
# Initialize new session
  local response
  response=$(curl -s -i -X POST "$PAPER_MCP_URL" \
    -H "Content-Type: application/json" \
    -H "Accept: $ACCEPT_HEADER" \
    -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"openclaw-paper-skill","version":"1.0.0"}},"id":1}' 2>&1)
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
fi

  # Send initialized notification
  curl -s -X POST "$PAPER_MCP_URL" \
    -H "Content-Type: application/json" \
    -H "Accept: $ACCEPT_HEADER" \
    -H "Mcp-Session-Id: $session_id" \
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
fi

  # Send initialized notification
  curl -s -X POST "$PAPER_MCP_URL" \
    -H "Content-Type: application/json" \
    -H "Accept: $ACCEPT_HEADER" \
    -H "Mcp-Session-Id: $session_id" \
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
fi

  # Send initialized notification
  curl -s -X POST "$PAPER_MCP_URL" \
    -H "Content-Type: application/json" \
    -H "Accept: $ACCEPT_HEADER" \
    -H "Mcp-Session-Id: $session_id" \
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script automatically decodes screenshot image data returned by the local Paper MCP server and writes it to disk under /tmp or an arbitrary caller-supplied path, without any confirmation, warning, or restriction. In an agent context, screenshots can contain sensitive design content or user data, and silent persistence increases the chance of unintended local data exposure to other processes or users.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The script stores the MCP session identifier in a persistent file under /tmp without setting restrictive permissions or warning the user. Although this is a local integration, /tmp is a shared location on many systems, so another local process or user could potentially read or tamper with the cached session state.

Static analysis

No suspicious patterns detected.