Back to skill

Security audit

File Sender

Security checks for vulnerabilities and agentic risk

Overview

This skill is a file-sharing helper, but it gives chat requests broad power to find and send sensitive local files with weak authorization safeguards.

Install only if you are comfortable with an agent searching across your readable filesystem and sending files, including sensitive documents and credentials, over chat. Before use, require a verified recipient allowlist, restrict searches to approved directories, remove anti-refusal instructions, and fix the credential temp-file and path-validation issues.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • 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
  • 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 (5)

T01 · Skill Instruction Hijacking

Error
Location
SKILL.md:63
Finding
Unverified Chat Identity Grants Unrestricted Access to Sensitive Local Files## Vulnerability Details **File Location**: `SKILL.md`, lines 63–84 **Vulnerability Type**: Authorization bypass through instruction-level identity assumptions **Risk Level**: Critical ### Vulnerable Code ```markdown ## Security Behavior **Marco (owner) has full access to ALL his files, including sensitive ones.** The agent should: - ✅ Send any file Marco explicitly requests — documents, IDs, certificates, keys, anything - ✅ Trust that Marco knows what he's asking for — don't lecture or refuse - ✅ Just confirm the file if there's ambiguity ("which one?") and send it The agent should NOT: - ❌ Refuse to send Marco's own files with "I can't send sensitive documents" - ❌ Add unnecessary warnings about security for routine requests - ❌ Block access to Marco's own data **Actual threats to protect against:** - Someone else in a group chat asking for Marco's files → refuse and notify Marco - External/unauthorized requests → verify identity before sending - Credentials in `~/Documenti/credentials/` → use encrypted send flow (cred_manager.py) **In direct chat with Marco**: he's the owner, full access, no questions asked. ``` ### Technical Analysis The Skill changes the agent's safety behavior by instructing it to suppress refusals and provide unrestricted access to all readable files, including credentials, certificates, keys, and identity documents. It treats the conversational identity “Marco” and the fact that a conversation is a direct chat as sufficient proof of authorization. No executable control binds “Marco” to an authenticated account identifier, approved channel, trusted device, or immutable recipient ID. The instruction to “verify identity” is not backed by any verification mechanism in either script. Consequently, authorization is delegated entirely to prompt interpretation and mutable chat context. This is an instruction-hijacking risk because loading the Skill replaces ordinary safeguards with a b ...[truncated 1257 chars]
Remediation
## Remediation Suggestions 1. Remove instructions that suppress refusals or grant a conversational identity unrestricted filesystem access. 2. Bind the owner to immutable, authenticated channel-specific identifiers stored in protected configuration. 3. Enforce owner and recipient authorization inside the scripts, not solely in `SKILL.md`. 4. Use an allowlist of approved recipient and channel combinations. 5. Require explicit confirmation for every sensitive-file transfer, including the exact path, destination, and data classification. 6. Deny private keys, authentication tokens, credential stores, and system files by default. 7. Ensure group-chat and direct-chat authorization decisions use verified platform metadata rather than user-provided claims. 8. Record auditable transfer events without logging file contents or secrets.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/send_file.py:58
Finding
Filesystem-Wide Discovery and Arbitrary-Recipient File Transmission## Vulnerability Details **File Location**: `SKILL.md`, lines 20–26 and 48–54; `scripts/send_file.py`, lines 58–84 **Vulnerability Type**: Excessive filesystem access and missing recipient authorization **Risk Level**: High ### Vulnerable Code ```markdown 2. **Search for the file** — Use `find` or `locate` to locate it. Search is **read-only**; never modify, move, or delete files. 3. **Confirm with the user** — If multiple matches exist, list them and ask which one to send. If the path is ambiguous, confirm before sending. 4. **Send the file** — Run the bundled script: ```bash python3 scripts/send_file.py "<file_path>" --target <chat_id> --channel <channel> ``` ``` ```markdown ## File Search Use shell commands to locate files. Prefer `find` with `-readable` flag: ```bash find / -name "filename*" -readable -type f 2>/dev/null ``` ``` ```python def send_file(file_path: str, target: str, channel: str = "telegram", force_document: bool = False) -> dict: resolved = resolve_path(file_path) if not os.path.exists(resolved): return {"error": f"File not found: {resolved}"} if not os.path.isfile(resolved): return {"error": f"Not a file: {resolved}"} if not os.access(resolved, os.R_OK): return {"error": f"Permission denied: {resolved}"} size_mb = os.path.getsize(resolved) / (1024 * 1024) if size_mb > 50: return {"error": f"File too large ({size_mb:.1f} MB). Telegram limit is 50 MB."} send_path, staged_dir = stage_file(resolved) cmd = [ "openclaw", "message", "send", "--channel", channel, "--target", target, "--media", send_path, ] if force_document: cmd.append("--force-document") ``` ### Technical Analysis The documented search procedure explicitly recommends searching from the filesystem root. The sending implementation accept ...[truncated 1771 chars]
Remediation
## Remediation Suggestions 1. Restrict file discovery and transmission to explicitly approved roots, such as a dedicated export directory. 2. Do not recommend `find /`; use a small set of configured directories and avoid returning sensitive paths in search results. 3. Resolve and canonicalize every source path, then verify with `os.path.commonpath` that it remains within an approved root. 4. Reject known-sensitive locations such as home-directory key stores, credential directories, browser profiles, environment files, and system configuration. 5. Bind each authorized user to an allowlisted channel and target in protected configuration. 6. Ignore or reject recipient destinations supplied solely through natural-language requests. 7. Add a separate approval step for files classified as sensitive. 8. Apply channel-specific size limits rather than enforcing the Telegram limit for every channel.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/cred_manager.py:167
Finding
Path Traversal in Credential Receive Name Allows Arbitrary File Overwrite## Vulnerability Details **File Location**: `scripts/cred_manager.py`, lines 167–197 **Vulnerability Type**: Path traversal and arbitrary writable-file truncation **Risk Level**: High ### Vulnerable Code ```python def receive_file(filepath: str, name: str = None): """Receive a file (e.g. from Telegram download), encrypt it, and store. The plaintext file is secure-deleted after successful encryption.""" filepath = os.path.expanduser(filepath) if not os.path.exists(filepath): print(f"ERROR: Not found: {filepath}", file=sys.stderr) sys.exit(1) os.makedirs(CRED_DIR, exist_ok=True) # Determine output name basename = name or os.path.basename(filepath) if not basename.endswith(AGE_EXT): basename += AGE_EXT output_path = os.path.join(CRED_DIR, basename) # Check for collision if os.path.exists(output_path): stem, ext = os.path.splitext(basename) i = 1 while os.path.exists(output_path): output_path = os.path.join(CRED_DIR, f"{stem}_{i}{ext}") i += 1 # Encrypt (pipe — plaintext never written to credentials dir) encrypt_stream(filepath, output_path) print(f"✅ Received & encrypted → {output_path}") print(f"🔒 Plaintext securely deleted") ``` The destination is subsequently opened for truncating output: ```python with open(input_path, "rb") as infile, open(output_path, "wb") as outfile: proc = subprocess.Popen( ["age", "-e", "-r", pubkey], stdin=infile, stdout=outfile, stderr=subprocess.PIPE ) ``` ### Technical Analysis The `--name` value is used as a path component without validation. It is not reduced to a filename, and the resolved output is not checked to ensure that it remains under `CRED_DIR`. A value containing `../` can traverse outside the credential directory. An absolute path is even more direct because `os.path.joi ...[truncated 1718 chars]
Remediation
## Remediation Suggestions 1. Reject absolute names and any name containing `/`, `\`, `..`, null bytes, or platform-specific path separators. 2. Reduce custom names to a validated filename, for example with `os.path.basename`, while still rejecting input that changes during normalization. 3. Resolve the final path and verify containment: ```python candidate = os.path.realpath(os.path.join(CRED_DIR, safe_name)) cred_root = os.path.realpath(CRED_DIR) if os.path.commonpath([candidate, cred_root]) != cred_root: raise ValueError("Destination escapes credential directory") ``` 4. Create destination files atomically and exclusively using `O_CREAT | O_EXCL | O_NOFOLLOW` where supported. 5. Write encryption output to a private temporary file in `CRED_DIR`, verify successful encryption, and atomically rename it into place. 6. Reject symlink destinations and verify the directory ownership and permissions. 7. Delete the plaintext only after the encrypted output has been safely committed and validated.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/send_file.py:29
Finding
Predictable Shared Staging Path Enables File Overwrite and Symlink Attacks## Vulnerability Details **File Location**: `scripts/send_file.py`, lines 29–48 **Vulnerability Type**: Unsafe temporary-file staging **Risk Level**: Medium ### Vulnerable Code ```python def stage_file(resolved: str) -> tuple[str, str | None]: """Stage file into workspace .tmp-send if outside it. openclaw only allows media paths under the workspace directory. Returns (staged_path, staged_dir or None).""" if os.path.commonpath([resolved, WORKSPACE]) == WORKSPACE: return resolved, None os.makedirs(TEMP_SEND_DIR, exist_ok=True) staged = os.path.join(TEMP_SEND_DIR, os.path.basename(resolved)) shutil.copy2(resolved, staged) return staged, TEMP_SEND_DIR def cleanup(send_path: str, original: str, staged_dir: str | None): """Remove staged files after send.""" if send_path != original and os.path.exists(send_path): try: os.remove(send_path) except OSError: pass if staged_dir and os.path.isdir(staged_dir): try: if not os.listdir(staged_dir): os.rmdir(staged_dir) except OSError: pass ``` ### Technical Analysis Files outside the workspace are staged under a shared path derived only from their basename. The path is predictable and is created without exclusive-open or no-follow protections. If `.tmp-send/<basename>` already exists, `shutil.copy2()` overwrites it. If it is a symbolic link, the copy operation can follow the link and overwrite its writable target. Cleanup later removes the staging pathname without confirming that it is the same object created by the current operation. Concurrent sends of files with the same basename can also overwrite one another, leading to incorrect-file disclosure or transfer failures. ### Attack Path 1. An attacker with write access to the workspace staging directory predicts the basename of a file that will be ...[truncated 1172 chars]
Remediation
## Remediation Suggestions 1. Create a unique private directory for every transfer using `tempfile.mkdtemp(dir=WORKSPACE)` and mode `0700`. 2. Create staged files exclusively and reject symbolic links. 3. Use random filenames rather than source basenames for local staging; provide the desired display filename separately if the messaging API supports it. 4. If the original basename must be preserved, place it inside the unique per-transfer directory. 5. Verify with `lstat` that the destination is a newly created regular file. 6. Put staging and command execution inside a `try/finally` block so cleanup occurs on copy and subprocess exceptions. 7. Track the exact temporary directory created by the current process and recursively remove only that directory. 8. Set restrictive permissions on the staging directory and staged file before copying sensitive content.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/cred_manager.py:137
Finding
Credential Send Flow Writes Decrypted Plaintext to Potentially Persistent Storage## Vulnerability Details **File Location**: `scripts/cred_manager.py`, lines 137–170 **Vulnerability Type**: Plaintext credential exposure through disk-backed temporary files **Risk Level**: Medium ### Vulnerable Code ```python def send_file(filepath: str, target: str, channel: str): """Decrypt to RAM, write to workspace temp, send, secure delete.""" # Verify openclaw is available if not shutil.which("openclaw"): print("ERROR: openclaw CLI not found in PATH", file=sys.stderr) sys.exit(1) try: plaintext = decrypt_to_bytes(filepath) except Exception as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) tmp_dir = TEMP_SEND_DIR basename = os.path.basename(filepath).removesuffix(AGE_EXT) ext_suffix = os.path.splitext(basename)[1] or ".bin" fd, tmp_path_raw = tempfile.mkstemp(prefix=f"cred-{basename}-", suffix=ext_suffix, dir=tmp_dir) try: os.write(fd, plaintext) os.close(fd) plaintext = b"\x00" * len(plaintext) # Rename to clean filename so the recipient gets the correct name tmp_path = os.path.join(os.path.dirname(tmp_path_raw), basename) os.rename(tmp_path_raw, tmp_path) os.chmod(tmp_path, 0o600) # Use send_file.py which handles allowed-path restrictions send_script = os.path.join(os.path.dirname(__file__), "send_file.py") cmd = [ sys.executable, send_script, tmp_path, "--target", target, "--channel", channel, "--force-document", ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode != 0: print(f"ERROR: {result.stderr}", file=sys.stderr) else: print(f"✅ Sent {basename} via {channel} to {target}") finally: secure_delete(tmp_path) ``` ### Technical Analysis Despite the statement th ...[truncated 1961 chars]
Remediation
## Remediation Suggestions 1. Use a verified RAM-backed filesystem such as `/dev/shm` where available, with a private mode-`0700` directory. 2. Fail closed or explicitly require user approval before falling back to persistent storage for credential plaintext. 3. Prefer streaming decryption directly into a messaging process if the receiving CLI supports standard input or file descriptors. 4. Avoid materializing the entire decrypted file as an immutable Python `bytes` object. 5. Ensure temporary files are mode `0600` from creation and temporary directories are mode `0700`. 6. Use randomized, collision-resistant paths and avoid renaming to a predictable basename. 7. Place file-descriptor closure and cleanup in robust `try/finally` blocks, including failures before `tmp_path` is assigned. 8. Correct the documentation so it does not claim memory-only handling unless that property is technically enforced. 9. Treat secure overwrite as best-effort rather than a reliable substitute for avoiding persistent plaintext storage.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (23)

Tp4

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding
The declaration presents a dual-purpose skill, primarily including general-purpose local file sending/sharing across multiple chat platforms, plus encrypted credential handling. This code chunk only substantiates the credential-management portion: encrypt/decrypt/list/store/send encrypted credential files using age. It operates on a fixed credentials store, relies on a local key file, and sends decrypted credential files through another script. There is no implemented logic here to locate arbitrary local files, no broad file-sharing workflow for normal documents, and no visible platform-specific handling for Telegram/Discord/WhatsApp/Signal/Slack beyond a generic channel parameter passed downstream. Because the declared description materially overstates the general file-sharing behavior relative to this code chunk, this is a mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description overstates the skill's behavior. The supplied code is a straightforward file-sending wrapper around `openclaw message send`. It can send a given local file path to a target and stage files into the workspace, but it does not 'find' files, does not handle credential triggers, and contains no logic for encrypted credential files or age encryption. The multi-platform claim is also only loosely supported: while `--channel` is accepted as a parameter, the implementation is tailored to Telegram with a hardcoded 50 MB 'Telegram limit' check, and there is no platform-specific handling for Discord, WhatsApp, Signal, or Slack. Therefore the description does not accurately represent the actual code behavior.

Vague Triggers

High
Confidence
98% confidence
Finding
The trigger phrases are broad enough to match ordinary conversation, making unintended invocation plausible for a skill that can search the filesystem and transmit files externally. In context, accidental activation is especially dangerous because the skill handles arbitrary local files and sensitive credentials.

Ssd 1

High
Confidence
98% confidence
Finding
The skill authorizes sending any sensitive file based largely on a claimed identity/ownership model and instructs the agent not to apply normal safety checks. In practice, this creates a direct exfiltration path for local secrets because a chat-context claim of being the owner is not strong authentication, especially in compromised or spoofed messaging scenarios.

Ssd 3

High
Confidence
98% confidence
Finding
The instructions explicitly tell the agent to reveal and transmit highly sensitive data such as IDs, certificates, keys, and password files. Because the core function is outbound transfer to chat channels, this is not incidental exposure but a designed exfiltration capability with inadequate safeguards.

Natural-Language Policy Violations

High
Confidence
97% confidence
Finding
These instructions explicitly suppress appropriate security warnings and direct the agent to send sensitive files with minimal safeguards. In a file-exfiltration context, discouraging refusal and caution materially increases the chance that secrets, IDs, keys, or financial records are transmitted to an attacker or the wrong destination.

Anti-Refusal Statement

High
Category
Anti-Refusal
Content
The agent should:
- ✅ Send any file Marco explicitly requests — documents, IDs, certificates, keys, anything
- ✅ Trust that Marco knows what he's asking for — don't lecture or refuse
- ✅ Just confirm the file if there's ambiguity ("which one?") and send it

The agent should NOT:
Confidence
96% confidence
Finding
The anti-refusal phrase 'don't lecture or refuse' is a classic attempt to neutralize an agent's safety controls. In this skill, it is particularly dangerous because it is attached to requests for arbitrary and highly sensitive local files, increasing the likelihood of policy bypass and unauthorized disclosure.

Intent-Code Divergence

High
Confidence
96% confidence
Finding
The documentation makes contradictory claims that plaintext credentials never touch disk while also describing downloads to /tmp, fallback tmpdirs, and decryption to filesystem-backed locations. Such inconsistencies are dangerous because they can lead operators to rely on protections that are not actually guaranteed, resulting in plaintext secret exposure through temp files, backups, swap, or forensic recovery.

Unvalidated Output Injection

High
Category
Output Handling
Content
sys.exit(1)
    output = output or filepath + AGE_EXT
    pubkey = get_public_key()
    proc = subprocess.run(
        ["age", "-e", "-r", pubkey, "-o", output, filepath],
        capture_output=True, text=True
    )
Confidence
95% confidence
Finding
The function accepts an arbitrary output path and writes encrypted data there without validation, enabling file placement or overwrite in attacker-chosen locations. In the context of a file-sending credential skill, this can be abused to clobber user files, create artifacts in sensitive directories, or bypass intended storage boundaries.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill invokes shell commands, reads arbitrary files, and documents workflows that write, move, encrypt, and delete files, yet it declares no explicit tool scope or permission boundaries. In an agent environment, this increases the risk of overbroad filesystem access and unintended execution because the runtime has no manifest-level constraints to limit what the skill may do.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The skill’s declared purpose is sending local files, but the documented workflow also accepts inbound uploads and persistently stores highly sensitive documents. That materially expands the data-handling surface into collection, storage, and lifecycle management of secrets, increasing exposure if the skill is triggered unexpectedly or used in the wrong context.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Security:
    - age X25519 + ChaCha20-Poly1305
    - Private key chmod 600, never leaves machine
    - Temp files in workspace (openclaw allowed dir), secure-deleted after use
    - receive() encrypts in a single pipe — plaintext never touches disk
"""
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Security:
    - age X25519 + ChaCha20-Poly1305
    - Private key chmod 600, never leaves machine
    - Temp files in workspace (openclaw allowed dir), secure-deleted after use
    - receive() encrypts in a single pipe — plaintext never touches disk
"""
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
Security:
    - age X25519 + ChaCha20-Poly1305
    - Private key chmod 600, never leaves machine
    - Temp files in workspace (openclaw allowed dir), secure-deleted after use
    - receive() encrypts in a single pipe — plaintext never touches disk
"""
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The documentation claims plaintext never touches disk during receive, but the implementation requires a plaintext file already present on disk and only encrypts it afterward. This misleading guarantee is dangerous in a credential-handling skill because operators may rely on a protection that does not actually exist, increasing the chance of sensitive secrets being stored, scanned, backed up, or recovered from disk.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
for line in f:
            if line.startswith("# public key: "):
                return line.split(": ", 1)[1].strip()
    proc = subprocess.run(["age-keygen", "-y", KEY_PATH], capture_output=True, text=True)
    return proc.stdout.strip()
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
sys.exit(1)
    output = output or filepath + AGE_EXT
    pubkey = get_public_key()
    proc = subprocess.run(
        ["age", "-e", "-r", pubkey, "-o", output, filepath],
        capture_output=True, text=True
    )
Confidence
88% confidence
Finding
The output path is passed directly to the encryption command with no restriction to a safe directory, allowing a caller to cause writes to arbitrary filesystem locations accessible to the process. In a skill whose purpose is handling local files and credentials, this broad write primitive is more dangerous because the agent may be induced to overwrite sensitive files or place encrypted blobs in unintended locations.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
Secure-deletes the input immediately after encryption succeeds."""
    pubkey = get_public_key()
    with open(input_path, "rb") as infile, open(output_path, "wb") as outfile:
        proc = subprocess.Popen(
            ["age", "-e", "-r", pubkey],
            stdin=infile, stdout=outfile, stderr=subprocess.PIPE
        )
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
filepath = os.path.expanduser(filepath)
    if not os.path.exists(filepath):
        raise FileNotFoundError(filepath)
    proc = subprocess.run(
        ["age", "-d", "-i", KEY_PATH, filepath],
        capture_output=True
    )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The tool deletes the original plaintext by default after encryption with no confirmation prompt, dry run, or explicit runtime warning. In an agent skill that acts on local files from natural-language requests, this is risky because an ambiguous or manipulated request could trigger irreversible destruction of user data or evidence.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
result = None
    for attempt in range(MAX_RETRIES):
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
            if result.returncode == 0:
                break
            last_error = f"openclaw returned {result.returncode}: {result.stderr.strip()}"
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
result = None
    for attempt in range(MAX_RETRIES):
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
            if result.returncode == 0:
                break
            last_error = f"openclaw returned {result.returncode}: {result.stderr.strip()}"
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Intent-Code Divergence

Low
Confidence
83% confidence
Finding
The function docstring says the 3-pass overwrite is 'more robust than single-pass, especially on SSDs,' but overwriting is not reliably effective on SSDs due to wear leveling and storage internals. This is not merely incomplete wording; it actively overstates what the implementation can guarantee.

Static analysis

No suspicious patterns detected.