Back to skill

Security audit

Joplin Notes

Security checks for vulnerabilities and agentic risk

Overview

The skill is clearly meant to manage Joplin notes, but it handles WebDAV credentials and note paths unsafely enough that users should review it before installing.

Install only if you trust the publisher and can restrict the WebDAV account to the Joplin directory. Use an HTTPS-only WebDAV URL, avoid broad account credentials, keep backups before write operations, and consider patching the scripts to validate IDs, avoid curl password arguments, use secure temporary files, and resolve bundled scripts relative to the skill directory.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Tool Hijacking and SpoofingModifies or replaces tools so legitimate-looking calls execute attacker logic
  • 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
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/list_notes.py:35
Finding
WebDAV Credentials Exposed in Process Arguments and Potentially Transmitted over Cleartext HTTP## Vulnerability Details **File Location**: `scripts/list_notes.py:35-38, 56-57`; `scripts/get_note.py:29-34`; `scripts/upsert_note.py:83-89` **Vulnerability Type**: Credential exposure and insecure transport configuration **Risk Level**: High ### Vulnerable Code `scripts/list_notes.py:35-38`: ```python command = [ "curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}", "-X", "PROPFIND", "--header", "Depth: infinity", url ] result = subprocess.run(command, capture_output=True, text=True, check=True) ``` `scripts/list_notes.py:56-57`: ```python command = ["curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}", f"{url}{filename}"] result = subprocess.run(command, capture_output=True, text=True, check=True) ``` `scripts/get_note.py:29-34`: ```python filename = f"{note_id}.md" command = [ "curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}", f"{url}{filename}" ] result = subprocess.run(command, capture_output=True, text=True) ``` `scripts/upsert_note.py:83-89`: ```python command = [ "curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}", "-X", "PUT", "-T", "-", f"{url}{filename}" ] result = subprocess.run(command, input=content.encode('utf-8'), capture_output=True) ``` ### Technical Analysis Each WebDAV operation inserts the value of `JOPLIN_PASSWORD` directly into the `curl` process argument list through the `-u username:password` option. Depending on operating-system process visibility and monitoring configuration, another local user or diagnostic service may be able to observe the complete argument list while `curl` is running. The scripts also accept `JOPLIN_WEBDAV_PATH` without validating its scheme. If it begins with `http://`, HTTP Basic authentication credentials and Joplin note contents are sent without transport encryption. Basic authentication only encodes credentials and does not provide confidentiality. ### Attack Path 1. A user configures `JOPLIN_W ...[truncated 840 chars]
Remediation
## Remediation Suggestions - Replace command-line `curl` execution with a maintained HTTP/WebDAV library that accepts credentials through an in-memory authentication API. - Require the configured URL to use the `https` scheme and reject cleartext HTTP. - Preserve TLS certificate and hostname verification; do not add insecure certificate-bypass options. - Validate the configured destination against an explicit host allowlist where deployment permits. - Reject URLs containing embedded credentials, unsupported schemes, fragments, or unexpected redirects. - If `curl` must remain in use, provide credentials through a protected mechanism that does not expose the password in the process argument list. - Use a WebDAV account limited to the required Joplin directory rather than a broadly privileged account.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/get_note.py:29
Finding
Unvalidated Note Identifiers Permit WebDAV Path Traversal## Vulnerability Details **File Location**: `scripts/get_note.py:29-34, 46`; `scripts/upsert_note.py:98-99, 119-120` **Vulnerability Type**: Authenticated remote path traversal and unauthorized resource access **Risk Level**: High ### Vulnerable Code `scripts/get_note.py:29-34`: ```python filename = f"{note_id}.md" command = [ "curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}", f"{url}{filename}" ] result = subprocess.run(command, capture_output=True, text=True) ``` `scripts/get_note.py:46`: ```python note_id = sys.argv[1].replace(".md", "") ``` `scripts/upsert_note.py:98-99`: ```python note_id = sys.argv[1] parent_id = sys.argv[2] ``` `scripts/upsert_note.py:119-120`: ```python filename = f"{note_id}.md" if upload_note(filename, full_content): ``` The resulting filename is subsequently appended directly to the base URL by `upload_note`: ```python command = [ "curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}", "-X", "PUT", "-T", "-", f"{url}{filename}" ] ``` ### Technical Analysis Joplin object IDs are expected to be fixed-format hexadecimal identifiers, but neither script validates that format. Instead, user-controlled values are directly concatenated with the configured WebDAV base URL. An identifier containing path separators and parent-directory components, such as `../shared/target`, produces a URL ending in `../shared/target.md`. If the client, reverse proxy, or WebDAV server normalizes the dot segment, the authenticated request escapes the configured Joplin directory. The read operation performs a GET, while the upsert operation performs a PUT and can therefore overwrite or create a resource. Using an argument list prevents shell command injection, but it does not prevent URL path manipulation. ### Attack Path 1. The attacker can influence the `note_id` supplied to `get_note.py` or `upsert_note.py`. 2. The attacker supplies a traversal valu ...[truncated 807 chars]
Remediation
## Remediation Suggestions - Validate existing Joplin note IDs with an allowlist expression such as `^[0-9a-f]{32}$`. - Continue generating new IDs internally with `uuid.uuid4().hex`. - Reject values containing `/`, `\`, `..`, percent-encoded characters, query delimiters, or fragments. - Treat an object ID as one URL path segment and encode it with a URL-quoting function that does not preserve path separators. - Resolve and normalize the final URL, then verify that its scheme, authority, and path remain under the configured WebDAV base directory. - Apply equivalent validation to notebook and parent IDs to preserve Joplin data integrity. - Restrict the WebDAV account to the Joplin directory as a defense-in-depth measure.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/create_notebook.py:13
Finding
Predictable Shared Temporary File Permits Symlink and Race Attacks## Vulnerability Details **File Location**: `scripts/create_notebook.py:13-23` **Vulnerability Type**: Insecure temporary-file creation **Risk Level**: Medium ### Vulnerable Code ```python def create_notebook(title, parent_notebook_id=""): # A notebook only needs a file with the title and metadata (Type 2) # A notebook normally doesn't have body content. temp_file = "/tmp/new_notebook_init.md" with open(temp_file, "w", encoding="utf-8") as f: f.write(title + "\n") # First line is the title # Call upsert with Type 2 (Notebook) cmd = ["python3", UPSERT_SCRIPT, "new", parent_notebook_id, temp_file, "2"] result = subprocess.run(cmd, capture_output=True, text=True) if os.path.exists(temp_file): os.remove(temp_file) ``` ### Technical Analysis The script uses a constant filename in a globally writable temporary directory. Python's ordinary `open(..., "w")` follows symbolic links and does not request exclusive creation. Consequently, an attacker with local access can create the path as a symbolic link before the script runs. The script then truncates and writes through the link with the privileges of the invoking user. The shared filename also creates a time-of-check/time-of-use and concurrency problem. Another process can replace the file between its creation and consumption by `upsert_note.py`, or simultaneous notebook-creation operations can overwrite and delete one another's input. ### Attack Path 1. A local attacker predicts the fixed path `/tmp/new_notebook_init.md`. 2. The attacker creates that path as a symbolic link to a file writable by the victim account. 3. The victim invokes `create_notebook.py`. 4. `open(temp_file, "w")` follows the symbolic link and truncates the target. 5. The script writes the notebook title into the target file. 6. Alternatively, the attacker swaps the temporary file before the delegated upsert reads it, causing at ...[truncated 342 chars]
Remediation
## Remediation Suggestions - Prefer eliminating the temporary file and passing the generated content directly to the upsert implementation. - If a file is required, use `tempfile.NamedTemporaryFile` or `tempfile.mkstemp` to create a unique file atomically with restrictive permissions. - Keep the returned file descriptor open while writing to prevent path replacement. - Place cleanup in a `finally` block and remove only the uniquely created file. - Do not perform a separate `exists` check before deletion. - Refactor notebook creation and note upsert into importable functions so that content can be transferred in memory without invoking another script.

T07 · Tool Hijacking and Spoofing

Warning
Location
scripts/create_notebook.py:7
Finding
Hard-Coded External Script Path Can Delegate Execution to an Unreviewed File## Vulnerability Details **File Location**: `scripts/create_notebook.py:7-8, 19-20` **Vulnerability Type**: Local tool substitution through unsafe script resolution **Risk Level**: Medium ### Vulnerable Code ```python # Paths to the other scripts SKILL_DIR = "/home/openclaw/.openclaw/workspace/skills/joplin-notes" UPSERT_SCRIPT = os.path.join(SKILL_DIR, "scripts/upsert_note.py") ``` ```python # Call upsert with Type 2 (Notebook) cmd = ["python3", UPSERT_SCRIPT, "new", parent_notebook_id, temp_file, "2"] result = subprocess.run(cmd, capture_output=True, text=True) ``` ### Technical Analysis `create_notebook.py` does not invoke the `upsert_note.py` file bundled beside it. It instead uses an absolute path associated with a separate installation location. Therefore, the code that executes at runtime may differ from the code reviewed in this project. Although the use of a subprocess argument list prevents shell metacharacter injection, it does not protect against substitution of the Python file at the hard-coded path. A stale, compromised, or attacker-created installation can execute arbitrary Python code with the invoking user's privileges. The child process also inherits the environment by default, including the Joplin credentials. ### Attack Path 1. An attacker gains write access to `/home/openclaw/.openclaw/workspace/skills/joplin-notes/scripts/upsert_note.py` or can create the expected path when it is absent. 2. The attacker replaces that file with a malicious Python program. 3. The victim runs the audited `scripts/create_notebook.py`. 4. The script invokes `python3` on the substituted external file. 5. The malicious program executes with the victim's operating-system privileges and inherits environment variables such as `JOPLIN_PASSWORD` and `JOPLIN_ACCOUNT`. ### Impact Assessment If an attacker can control the hard-coded path, exploitation permits arbitrary Python execution with the privileges of the ...[truncated 243 chars]
Remediation
## Remediation Suggestions - Resolve the bundled sibling relative to the current source file: ```python from pathlib import Path UPSERT_SCRIPT = Path(__file__).resolve().parent / "upsert_note.py" ``` - Verify that the resolved target is a regular file and remains inside the expected Skill directory. - Avoid delegation through a second Python interpreter by moving shared behavior into an importable module and calling it directly. - Ensure the Skill directory and scripts are not writable by less-privileged or unrelated users. - Use a controlled interpreter path or the current interpreter through `sys.executable`. - Minimize the environment passed to child processes if subprocess execution remains necessary.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (22)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill documentation instructs use of a sensitive credential (JOPLIN_PASSWORD) and operational workflows, yet omits any declared permissions model and does not clearly surface that credentialed network access is required. In agent settings, this mismatch is dangerous because it can cause hidden credential use and broaden trust beyond what the metadata communicates.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill documentation instructs use of a sensitive credential (JOPLIN_PASSWORD) and operational workflows, yet omits any declared permissions model and does not clearly surface that credentialed network access is required. In agent settings, this mismatch is dangerous because it can cause hidden credential use and broaden trust beyond what the metadata communicates.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The skill documentation instructs use of a sensitive credential (JOPLIN_PASSWORD) and operational workflows, yet omits any declared permissions model and does not clearly surface that credentialed network access is required. In agent settings, this mismatch is dangerous because it can cause hidden credential use and broaden trust beyond what the metadata communicates.

Lp3

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding
The skill advertises executable scripts that use environment variables, filesystem access, network/WebDAV access, and shell execution patterns, but it does not declare any tool scope or permissions boundaries. This creates a transparency and governance gap: an agent or reviewer cannot easily determine what sensitive capabilities the skill requires before use, increasing the chance of unintended credential use or data modification.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The documentation presents create and update operations on notes and notebooks as routine usage without warning that they will modify user data. In an autonomous or semi-autonomous agent context, that omission increases the chance of accidental destructive actions, overwrites, or unintended notebook creation.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# Call upsert with Type 2 (Notebook)
    cmd = ["python3", UPSERT_SCRIPT, "new", parent_notebook_id, temp_file, "2"]
    result = subprocess.run(cmd, capture_output=True, text=True)
    
    if os.path.exists(temp_file):
        os.remove(temp_file)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Context-Inappropriate Capability

Medium
Confidence
83% confidence
Finding
The manifest describes a skill for managing Joplin notes via WebDAV, which justifies network access to the WebDAV endpoint. However, this implementation additionally depends on reading secrets from environment variables and spawning an external command-line tool to perform the request, which are broader execution capabilities not stated in the skill purpose itself.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script sends credentials using curl's -u option to whatever URL is present in the environment, without enforcing HTTPS or validating the destination. In this skill context, the network access is expected, but the lack of transport-security checks makes credential exposure more dangerous because a misconfigured or malicious JOPLIN_WEBDAV_PATH could leak the user's Joplin username and password to an attacker-controlled or plaintext endpoint.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}",
        f"{url}{filename}"
    ]
    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode != 0 or "404" in result.stdout:
        print(f"Error: Note with ID {note_id} not found.", file=sys.stderr)
        sys.exit(1)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'command' from os.getenv (line 31, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
"curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}",
        f"{url}{filename}"
    ]
    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode != 0 or "404" in result.stdout:
        print(f"Error: Note with ID {note_id} not found.", file=sys.stderr)
        sys.exit(1)
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}",
        "-X", "PROPFIND", "--header", "Depth: infinity", url
    ]
    result = subprocess.run(command, capture_output=True, text=True, check=True)
    
    # Extract path part of the URL for the regex
    url_path = urlparse(url).path
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}",
        "-X", "PROPFIND", "--header", "Depth: infinity", url
    ]
    result = subprocess.run(command, capture_output=True, text=True, check=True)
    
    # Extract path part of the URL for the regex
    url_path = urlparse(url).path
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'command' from os.getenv (line 53, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
"curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}",
        "-X", "PROPFIND", "--header", "Depth: infinity", url
    ]
    result = subprocess.run(command, capture_output=True, text=True, check=True)
    
    # Extract path part of the URL for the regex
    url_path = urlparse(url).path
Confidence
75% confidence
Finding
The WebDAV endpoint and credentials come from environment variables and are used to make outbound requests with curl. While this does not create shell injection, it can enable server-side request forgery–style behavior or unintended exfiltration if an attacker can influence the environment and redirect the skill to a malicious host that receives the Joplin credentials.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This function fetches note content from a remote WebDAV endpoint using stored credentials, which can transmit user note data over the network. While the script contains error messages, it does not include any user-facing warning, confirmation, or explanatory comment/docstring disclosing that note contents will be retrieved from a remote service.

Tainted flow: 'command' from os.getenv (line 53, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
url += "/"
        
    command = ["curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}", f"{url}{filename}"]
    result = subprocess.run(command, capture_output=True, text=True, check=True)
    return result.stdout

def parse_note_data(content):
Confidence
70% confidence
Finding
This request uses attacker-influenceable configuration for the destination URL and fetches note content from that remote endpoint. In the context of a note-management skill, redirecting requests to an untrusted server could expose credentials and sensitive note data or cause unintended access to arbitrary internal/external resources.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code performs a network upload of user-provided note content to a remote WebDAV endpoint using credentials from environment variables. While the script prints errors and success messages, it does not disclose before execution that it will send local file contents and authentication data to an external service.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}",
        "-X", "PUT", "-T", "-", f"{url}{filename}"
    ]
    result = subprocess.run(command, input=content.encode('utf-8'), capture_output=True)
    if result.returncode != 0:
        print(f"Error during upload: {result.stderr.decode()}", file=sys.stderr)
        sys.exit(1)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'command' from os.getenv (line 84, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
"curl", "-s", "-u", f"{JOPLIN_USERNAME}:{password}",
        "-X", "PUT", "-T", "-", f"{url}{filename}"
    ]
    result = subprocess.run(command, input=content.encode('utf-8'), capture_output=True)
    if result.returncode != 0:
        print(f"Error during upload: {result.stderr.decode()}", file=sys.stderr)
        sys.exit(1)
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Low
Confidence
80% confidence
Finding
Listing required credentials without any handling or privacy guidance normalizes passing secrets into scripts without telling users how they are protected, stored, or exposed. In a skill that uses networked storage, poor credential hygiene can lead to accidental leakage through logs, shell history, or misconfigured environments.

Missing User Warnings

Low
Confidence
88% confidence
Finding
The code writes notebook initialization data to a fixed path in /tmp and later deletes it, which creates a predictable temporary-file race/symlink risk on multi-user systems. An attacker could pre-create or replace /tmp/new_notebook_init.md to cause unintended file overwrite, data exposure, or deletion of an arbitrary file accessible to the process.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The function reads the JOPLIN_PASSWORD environment variable, which is a sensitive credential source. The code validates presence but does not provide any explanatory comment, docstring, or user-facing notice that credentials will be accessed and used for remote authentication.

Context-Inappropriate Capability

Low
Confidence
81% confidence
Finding
The manifest describes note and notebook management over WebDAV, but does not mention accessing process environment variables for credentials. While authentication is expected for WebDAV access, reading secrets from environment variables is an additional sensitive capability not justified by the stated purpose alone.

Static analysis

No suspicious patterns detected.