Back to skill

Security audit

Joplin API(中文)

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Joplin note helper, but its SSH remote mode can turn crafted note arguments into command execution on the configured remote host.

Install only if you plan to use local Joplin access or you fully trust and control any SSH target. Avoid SSH remote mode until the scripts validate IDs and fields and stop interpolating URLs and tokens into remote shell commands. Review write and delete actions carefully, especially permanent deletion.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/joplin.py:56
Finding
Remote Shell Command Injection Through Unescaped Joplin Request URL in Python Client<![CDATA[ ## Vulnerability Details **File Location**: `scripts/joplin.py`, lines 56-86 **Vulnerability Type**: Remote shell command injection **Risk Level**: High ### Vulnerable Code ```python def api_req(method, path, data=None): if HOST: # Remote Joplin via SSH url = build_url("http://127.0.0.1:41184", path) if data: # Base64 encode body to avoid shell quote swallowing b64 = base64.b64encode(data.encode("utf-8")).decode("ascii") # Use env vars + bash -c to avoid zsh globbing on remote macOS # Inside single quotes, $ is literal — no backslash needed remote_cmd = ( "JOP_URL='" + url + "' JOP_BODY='" + b64 + "' JOP_METHOD=" + method + " " "bash -c 'curl -s -X $JOP_METHOD " "-H \"Content-Type: application/json\" " "-d \"$(echo \"$JOP_BODY\" | base64 -d)\" " "\"$JOP_URL\"'" ) else: # Use env var to avoid zsh globbing on remote macOS remote_cmd = ( "JOP_URL='" + url + "' JOP_METHOD=" + method + " " "bash -c 'curl -s -X $JOP_METHOD " "\"$JOP_URL\"'" ) # SECURITY: remote_cmd is constructed from hardcoded curl invocations. # JOP_URL/JOP_BODY/JOP_METHOD are env-var indirection to bypass zsh globbing. # No raw user input reaches the shell — method is whitelisted (GET/POST/PUT/DELETE), # body is base64-encoded, URL is built by build_url() above. result = subprocess.run( ["ssh", HOST, remote_cmd], capture_output=True, text=True, check=True ) ``` ### Technical Analysis In remote mode, the script concatenates `url` into a shell command as a single-quoted environment assignment: ```text JOP_URL='<URL>' ... ``` The URL includes API paths assembled from command-line arguments. These include note IDs, folder IDs, requested field list ...[truncated 2576 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Eliminate dynamic remote-shell command construction.** Send request metadata and content through SSH standard input to a fixed, audited remote helper rather than concatenating them into a command string. 2. **Use strict identifier validation.** Validate note and folder IDs against the format expected by Joplin, such as `^[0-9a-fA-F]{32}$`, before constructing a request. 3. **Allowlist fields.** Parse requested fields and accept only documented Joplin field names. 4. **Validate numeric arguments.** Require limits to be integers within the Joplin API range, normally 1 through 100. 5. **Encode components separately.** Apply URL encoding to each path segment and query value rather than encoding or quoting a completed URL. 6. **Avoid tokens in command text.** Transfer the token through standard input or another protected channel so it is not embedded in the remote shell command or exposed through process inspection and logging. 7. **If shell use is unavoidable, apply a proven POSIX shell-quoting routine** to every dynamic value, including the URL, token, host-derived values, method, and body. Validation should still be applied as defense in depth. 8. **Add regression tests** using apostrophes, command separators, substitutions, newlines, and malformed IDs to confirm that no input can alter the remote command structure. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/joplin.sh:63
Finding
Remote Shell Command Injection Through Unescaped Joplin Request URL in Bash Client<![CDATA[ ## Vulnerability Details **File Location**: `scripts/joplin.sh`, lines 63-79 **Vulnerability Type**: Remote shell command injection **Risk Level**: High ### Vulnerable Code ```bash api_req() { local method="$1" local path="$2" local data="${3:-}" if [[ "$REMOTE" == true ]]; then local url url=$(build_url "http://127.0.0.1:41184" "$path") if [[ -n "$data" ]]; then # Base64 encode body to avoid shell quote swallowing on remote local b64 b64=$(printf '%s' "$data" | base64 -w 0) # Use env vars + bash -c to avoid zsh globbing on remote macOS ssh "$HOST" "JOP_URL='${url}' JOP_BODY='${b64}' JOP_METHOD=${method} bash -c 'curl -s -X \$JOP_METHOD -H \"Content-Type: application/json\" -d \"\$(echo \"\$JOP_BODY\" | base64 -d)\" \"\$JOP_URL\"'" else # Use env var to avoid zsh globbing on remote macOS ssh "$HOST" "JOP_URL='${url}' JOP_METHOD=${method} bash -c 'curl -s -X \$JOP_METHOD \"\$JOP_URL\"'" fi else ``` ### Technical Analysis The Bash implementation builds a complete remote shell command inside a double-quoted local string. The generated command places `url` inside a single-quoted remote-shell assignment: ```text JOP_URL='<URL>' ``` Although local expansion preserves the value as part of one argument to `ssh`, the SSH server subsequently passes that argument to the remote account's shell. An apostrophe in `url` can therefore close the remote single-quoted assignment and expose following shell syntax for execution. The URL includes API paths created from arguments such as note IDs, folder IDs, field lists, and limits. These values are not strictly validated before being placed into the URL. The API token is also appended without shell-safe quoting. Encoding the request body with Base64 does not protect these URL components. The Base64 behavior is functionally justified as a mechanism for transferring JSON through the documented SSH mode. No undeclared recipient or covert output ...[truncated 1610 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Do not compose SSH commands by string interpolation.** Use a fixed remote helper and send the URL and request body over standard input using a defined serialization format. 2. **Validate Joplin identifiers.** Reject note and folder IDs that do not match the expected 32-character hexadecimal format. 3. **Allowlist query fields** against documented Joplin property names. 4. **Restrict limits to integers in an accepted range**, normally 1 through 100. 5. **Percent-encode every path segment and query parameter independently.** 6. **Keep the API token out of the SSH command line.** Supply it through a protected input channel or a securely configured remote environment. 7. **If a remote shell cannot be removed, use a robust shell-escaping function** for every dynamic value. Do not rely on surrounding untrusted values with literal apostrophes. 8. **Constrain the SSH account.** Use a dedicated account, forced command, restricted shell, and minimal filesystem permissions so that compromise cannot provide general-purpose remote execution. 9. **Add injection-focused tests** covering apostrophes, semicolons, command substitutions, newlines, whitespace, and other shell metacharacters. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (32)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
Custom ID: supply 32-char hex string as `id` property.

### Delete Note (DELETE /notes/:id)

Default: move to trash. Add `?permanent=1` for permanent deletion.
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
Default: move to trash. Add `?permanent=1` for permanent deletion.

### DELETE /notes/:id/revisions

Delete all revisions for a note.
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
**Note:** When no `fields` parameter is specified, the API may return a tree structure with `children` key. When `fields` is specified, it returns a flat paginated list.

### DELETE /folders/:id

Default: move to trash. Add `?permanent=1` for permanent deletion.
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
Add tag to a note. Note data must contain at least an `id` property.

### DELETE /tags/:id/notes/:note_id

Remove tag from note.
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Credential Access

High
Category
Privilege Escalation
Content
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/../.env"

# ── Load .env ──────────────────────────────────────────────

if [[ ! -f "$ENV_FILE" ]]; then
  echo "Error: $ENV_FILE not found. Copy .env.example to .env and set api_token." >&2
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill invokes shell commands, reads local files, and performs network access to a local or SSH-forwarded Joplin API, yet it declares no explicit tool scope or allowed-tools boundary. Without a least-privilege declaration, an agent runtime may grant broader capabilities than necessary, increasing the chance of unintended file access, command execution, or network use beyond the documented Joplin workflow.

Session Persistence

Medium
Category
Rogue Agent
Content
---
name: joplin-api
description: 通过 Joplin Data API(Clipper Server)查询和管理笔记、笔记本、标签。支持本地直连和 SSH 远程两种模式。包含读写删全部操作,写/删需用户确认。触发词:"查 Joplin"、"Joplin 笔记"、"joplin search"、"create note in joplin" 等。
version: 1.4.1
metadata:
  openclaw:
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.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The trigger description includes broad phrases like managing notes and examples such as 'create note in joplin', but it does not narrowly constrain when the skill should activate or what safety checks must precede sensitive operations. Over-broad activation increases the risk that an agent invokes a capability-bearing skill in contexts the user did not intend, especially since the skill supports create, update, delete, shell, and remote network behaviors.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### API Token 安全
- Token 通过 URL 查询参数传递(`?token=xxx`),这是 Joplin Clipper API 的设计
- 注意:URL 可能被记录在 shell 历史、SSH 日志、代理日志中
- 建议:`.env` 文件权限设为 `600`(`chmod 600 .env`),并通过 `.gitignore` 排除

### 删除操作警告
- `delete <id>` 默认**软删除**(移至回收站,可在 Joplin 中恢复)
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This markdown file describes deleting notes, permanently deleting them with `?permanent=1`, and deleting all note revisions, but it does not include any warning about irreversibility or potential data loss. Under the markdown-specific warning rule, destructive behaviors that can affect user data should be accompanied by a clear caution.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The documentation states that folders can be deleted and permanently deleted via `?permanent=1`, but it provides no caution about the impact on user data or the irreversible nature of permanent deletion. Markdown skill documentation should warn users about behaviors affecting stored content or system integrity.

External Transmission

Medium
Category
Data Exfiltration
Content
Uses `multipart/form-data`:

```bash
curl -F 'data=@/path/to/file.jpg' \
     -F 'props={"title":"my resource title"}' \
     "http://localhost:41184/resources?token=$JOPLIN_TOKEN"
```
Confidence
60% 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
# Inside single quotes, $ is literal — no backslash needed
            remote_cmd = (
                "JOP_URL='" + url + "' JOP_BODY='" + b64 + "' JOP_METHOD=" + method + " "
                "bash -c 'curl -s -X $JOP_METHOD "
                "-H \"Content-Type: application/json\" "
                "-d \"$(echo \"$JOP_BODY\" | base64 -d)\" "
                "\"$JOP_URL\"'"
Confidence
92% confidence
Finding
This skill transmits Joplin note data, including note bodies and API tokens in URLs, over HTTP to a local or SSH-forwarded service and via remote curl execution. In the skill context, the tool is explicitly designed to read, create, update, and delete notes, so external transmission is expected; however, it still expands the attack surface because sensitive note content may be exposed through command arguments, process inspection, logs, or an insecure base_url if reconfigured away from localhost.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# JOP_URL/JOP_BODY/JOP_METHOD are env-var indirection to bypass zsh globbing.
        # No raw user input reaches the shell — method is whitelisted (GET/POST/PUT/DELETE),
        # body is base64-encoded, URL is built by build_url() above.
        result = subprocess.run(
            ["ssh", HOST, remote_cmd],
            capture_output=True, text=True, check=True
        )
Confidence
89% confidence
Finding
The code builds a remote shell command string and passes it to ssh for execution on the remote host. Although the author comments that no raw user input reaches the shell, the URL contains path/query components derived from command arguments such as note IDs, fields, folder IDs, and limits, and these values are embedded into shell-quoted environment assignments without robust escaping. A crafted value containing shell metacharacters or quotes could break out on the remote side and lead to command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def cmd_ping():
    if HOST:
        try:
            result = subprocess.run(
                ["ssh", HOST, "curl -s http://127.0.0.1:41184/ping"],
                capture_output=True, text=True, check=True
            )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

External Transmission

Medium
Category
Data Exfiltration
Content
local b64
      b64=$(printf '%s' "$data" | base64 -w 0)
      # Use env vars + bash -c to avoid zsh globbing on remote macOS
      ssh "$HOST" "JOP_URL='${url}' JOP_BODY='${b64}' JOP_METHOD=${method} bash -c 'curl -s -X \$JOP_METHOD -H \"Content-Type: application/json\" -d \"\$(echo \"\$JOP_BODY\" | base64 -d)\" \"\$JOP_URL\"'"
    else
      # Use env var to avoid zsh globbing on remote macOS
      ssh "$HOST" "JOP_URL='${url}' JOP_METHOD=${method} bash -c 'curl -s -X \$JOP_METHOD \"\$JOP_URL\"'"
Confidence
88% confidence
Finding
In remote mode, note content and the API token are transmitted over SSH to another host and then sent to the remote Joplin Clipper endpoint. This is intentional functionality, but it still expands the trust boundary: sensitive note contents and credentials leave the local machine and may be exposed on the remote host through process arguments, shell history, logging, or compromise of that host.

Static analysis

No suspicious patterns detected.