Back to skill

Security audit

Telegram File Browser

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Telegram file browser, but it needs Review because its callback handling and file-transfer paths can expose local files or run unsafe commands.

Install only if you are comfortable giving the skill access to browse and send files from the OpenClaw workspace through Telegram. It should be fixed before normal use: callbacks should be passed as safely quoted arguments or argv, callback tokens should be bound to the active session/version, preview/download should revalidate resolved paths against the root and handle symlinks safely, and file downloads should require clear user intent.

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

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:112
Finding
Shell Command Injection Through Unquoted Callback Concatenation<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:112-138` **Vulnerability Type**: Shell command injection **Risk Level**: High ### Vulnerable Code ```python # 1. Handle callback result = exec(command="python3 ~/.openclaw/workspace/skills/telegram-file-browser/scripts/browser_dispatcher.py handle-callback " + callback_data) response = json.loads(result.stdout) ``` The accompanying callback detection instructions state: ```text - if inbound text matches `^tfb_(root|dir|preview|path|download|back|close)_` - treat it as a telegram-file-browser callback - run `browser_dispatcher.py handle-callback <that_text>` immediately ``` ### Technical Analysis The Skill instructs an agent to concatenate attacker-influenced `callback_data` directly into a command string passed to the `exec` tool. No shell quoting or complete callback validation is applied before constructing the command. The documented detection expression only verifies that inbound text starts with an accepted prefix. It is not anchored at the end and does not prevent shell metacharacters, substitutions, redirections, or additional commands from appearing after the valid-looking prefix. If the OpenClaw `exec` tool evaluates the supplied command through a shell, characters such as command separators or command substitutions in the inbound callback can be interpreted by that shell rather than being passed only as an argument to `browser_dispatcher.py`. The Python implementation itself uses argument arrays when launching its child processes, but that protection does not cover the vulnerable command construction prescribed by `SKILL.md`. ### Attack Path 1. An attacker sends a Telegram text message beginning with a recognized callback prefix, such as `tfb_preview_`. 2. The message appends shell syntax after the accepted prefix. 3. Following the Skill instructions, the agent treats the entire inbound message as callback data. 4. The agent concatenates that data into the `exec(command="...") ...[truncated 838 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not concatenate callback data into a shell command. 2. Invoke the dispatcher with an argument-array API where the callback is passed as a distinct argument, for example conceptually: ```python exec_argv([ "python3", dispatcher_path, "handle-callback", callback_data, ]) ``` 3. If only a command-string API is available, use a trusted argument-quoting function such as `shlex.quote`; argument-array execution remains preferable. 4. Validate the complete callback against anchored, action-specific formats before execution. Reject whitespace, shell metacharacters, control characters, and unexpected suffixes. 5. Do not use a prefix-only expression as the security boundary. Use full matching, bounded callback length, and explicit permitted character sets. 6. Update all examples in `SKILL.md` so agents are never instructed to interpolate inbound data into command strings. 7. Add tests containing command separators, substitutions, quotes, newlines, and redirections to confirm that malicious callback text is rejected or passed literally. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/build_view.py:29
Finding
Workspace Root Boundary Bypass Through File Symlinks<![CDATA[ ## Vulnerability Details **File Location**: `scripts/build_view.py:29-39`, `scripts/browser_controller.py:81-96`, `scripts/run_browser_action.py:108-157`, `scripts/preview_file.py:29-53` **Vulnerability Type**: Improper path authorization and symlink traversal **Risk Level**: High ### Vulnerable Code Items are recorded using their unresolved directory-entry paths: ```python def build_items(entries: List[Path], prefix: str, start_index: int = 1) -> List[Dict[str, str]]: items = [] for i, entry in enumerate(entries, start=start_index): items.append({ "id": f"{prefix}{i}", "name": entry.name, "path": str(entry), "type": "dir" if entry.is_dir() else "file" }) return items ``` File actions are opened without resolving the target or confirming that it remains under the configured root: ```python def open_file_actions(state: Dict[str, Any], item_id: str) -> Dict[str, Any]: item = find_item(state, item_id) if not item or item.get("type") != "file": raise ValueError(f"File item not found: {item_id}") state["selectedFileId"] = item_id state["selectedFilePath"] = item["path"] bump_menu_version(state) ``` Preview and download operations trust the stored item path: ```python if action == 'preview': item = resolution.get('item') if not item: return {'toolAction': 'noop', 'message': 'preview target not found', 'state': state} preview = run_json(['python3', str(PREVIEW), item['path']]) ``` ```python if action == 'download': item = resolution.get('item') if not item: return {'toolAction': 'noop', 'message': 'download target not found', 'state': state} path = Path(item['path']) if not path.exists() or path.is_dir(): return { 'toolAction': 'send', 'message': f"⚠️ 无法下载:目标不存在或是目录\n{path_label(item['path'])}", 'replyTo': str(live_message_id), 'state': load_state(s ...[truncated 2906 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Centralize target authorization in one function used immediately before every preview, path, metadata, and download operation: ```python def authorize_target(root_value: str, target_value: str) -> Path: root = Path(root_value).expanduser().resolve(strict=True) target = Path(target_value).expanduser().resolve(strict=True) if not target.is_relative_to(root): raise PermissionError("Target escapes configured root") return target ``` 2. Pass the configured root to `preview_file.py` and validate the resolved target there as defense in depth. 3. Resolve and validate download targets immediately before constructing the message-tool payload. 4. Decide on an explicit symlink policy. The safest option is to reject all symlinks using `is_symlink()` and avoid following symlinked path components. 5. Revalidate authorization at the time of use because files and links can change after directory enumeration. 6. Validate path and file-type operations against the same resolved object. 7. Avoid relying on state-stored paths as authorization evidence; treat persistent state as untrusted input. 8. Add tests for file symlinks, directory symlinks, chained symlinks, broken links, and targets replaced after listing. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/resolve_callback.py:30
Finding
Stale Item Callbacks Bypass Menu-Version Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/resolve_callback.py:30-47` **Vulnerability Type**: Improper callback authorization and stale-request replay **Risk Level**: Medium ### Vulnerable Code ```python # Extract item_id from callback regardless of version. # Current formats: # tfb_root_v{version}_{item_id} # tfb_dir_{prefix}_v{version}_{item_id} item_id = None m = re.match(r"^tfb_root_v\d+_(w\d+)$", callback) if m: item_id = m.group(1) else: m = re.match(r"^tfb_dir_[^_]+_v\d+_([a-z\d]+)$", callback) if m: item_id = m.group(1) if item_id is not None: item = find_item(state, item_id) if item is not None: action = "open-dir" if item.get("type") == "dir" else "open-file-actions" print(json.dumps({"action": action, "item": item, "note": "processed despite version mismatch"}, ensure_ascii=False, indent=2)) return ``` The item lookup searches all retained views: ```python def find_item(state: dict, item_id: str) -> Optional[dict]: for view in state.get("views", {}).values(): for item in view.get("items", []): if item.get("id") == item_id: return item return None ``` ### Technical Analysis Item callbacks are parsed and resolved before the general menu-version validation is performed. The callback's version component is accepted as `\d+` but never compared with `state["menuVersion"]` on this execution path. The code explicitly returns the result with the note `processed despite version mismatch`. This contradicts the Skill's documented requirement to reject stale callbacks. The risk is increased by globally searching every retained view for an item ID. Item identifiers are short and are not globally unique: root IDs use positional values such as `w1`, while directory prefixes are generated elsewhere from a path hash reduced modulo 1000. Consequently, retained views can contain repeated or colliding identifiers, and a stale callback may reso ...[truncated 1432 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse and validate the callback version before resolving any action or item. 2. Reject every callback whose version differs from the current `menuVersion`, including root and directory item callbacks. 3. Bind each item callback to a specific view identifier rather than searching all views. 4. Replace positional and truncated-hash item IDs with cryptographically random, globally unique identifiers. 5. Store a callback-to-item mapping scoped to the current menu version. 6. Remove obsolete mappings when a menu is replaced, or mark them invalid by version. 7. Use `re.fullmatch` instead of prefix-oriented matching where possible. 8. Add replay tests covering stale item callbacks, pagination changes, retained views, ID collisions, and repeated callbacks outside the debounce window. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/preview_file.py:12
Finding
Unbounded Full-File Reads Allow Memory Exhaustion During Preview<![CDATA[ ## Vulnerability Details **File Location**: `scripts/preview_file.py:12-18`, `scripts/preview_file.py:53-61` **Vulnerability Type**: Resource exhaustion through unbounded file reads **Risk Level**: Medium ### Vulnerable Code ```python def looks_textual(path: Path) -> bool: if path.suffix.lower() in TEXT_EXTENSIONS: return True try: chunk = path.read_bytes()[:1024] except Exception: return False return b"\x00" not in chunk ``` ```python text = path.read_text(errors="replace") lines = text.splitlines() clipped_lines = lines[:args.max_lines] clipped_text = "\n".join(clipped_lines) truncated = False if len(lines) > args.max_lines: truncated = True if len(clipped_text) > args.max_chars: clipped_text = clipped_text[:args.max_chars] truncated = True ``` ### Technical Analysis The apparent preview bounds are applied only after the entire file has been loaded and split into lines. `path.read_bytes()[:1024]` does not read only 1,024 bytes. `read_bytes()` first loads the complete file, after which slicing retains the first 1,024 bytes. Similarly, `read_text()` loads and decodes the complete file before the line and character limits are applied. `splitlines()` can then allocate further memory proportional to the file size. Files with recognized text extensions skip the initial binary check but are still fully read. Therefore, the `--max-lines` and `--max-chars` options constrain only the returned output, not memory or processing consumption. ### Attack Path 1. An attacker places a very large file within the browsable workspace or causes such a file to be available through an accessible path. 2. The browser lists the file. 3. A user or agent selects the preview action. 4. `preview_file.py` loads the entire file into memory. 5. Text decoding and `splitlines()` create additional allocations. 6. The preview subprocess consumes excessive memory and CPU. 7. The process may terminate, become unresponsive, or ...[truncated 599 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Open files explicitly and read only a bounded amount: ```python MAX_PREVIEW_BYTES = 64 * 1024 with path.open("rb") as stream: raw = stream.read(MAX_PREVIEW_BYTES + 1) truncated = len(raw) > MAX_PREVIEW_BYTES raw = raw[:MAX_PREVIEW_BYTES] text = raw.decode("utf-8", errors="replace") ``` 2. Use a bounded read for textual detection rather than `read_bytes()`. 3. Check file size with `stat()` and reject or provide metadata-only output for files exceeding a configured threshold. 4. Limit line processing while streaming instead of calling `splitlines()` on the entire file. 5. Apply subprocess timeouts and operating-system memory limits as defense in depth. 6. Handle special files such as FIFOs, devices, and sockets safely; regular-file checks should occur before reading. 7. Add tests using sparse files, large single-line files, large multi-line files, and files with misleading text extensions. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (20)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill includes generic JSON-plan validation and command-line dispatch behavior that is broader than a simple Telegram browser UI description suggests. Even if intended as implementation detail, this broadens the operational surface and may enable unreviewed message construction or shell-driven flows beyond what the declared purpose communicates.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The skill includes generic JSON-plan validation and command-line dispatch behavior that is broader than a simple Telegram browser UI description suggests. Even if intended as implementation detail, this broadens the operational surface and may enable unreviewed message construction or shell-driven flows beyond what the declared purpose communicates.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill clearly instructs use of shell execution, local state files, and filesystem-oriented scripts, but it does not declare any explicit tool scope or allowed-tools boundary. That omission weakens reviewability and can let an agent invoke broader file and shell capabilities than users or platform policy may expect.

Session Persistence

Medium
Category
Rogue Agent
Content
3. Use `payload['message']` and `payload['buttons']` from the validated result

**Common mistakes to avoid:**
- ❌ Don't rewrite buttons manually
- ❌ Don't flatten buttons into one row
- ❌ Don't pass buttons as a JSON string
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.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
- if inbound text matches `^tfb_(root|dir|preview|path|download|back|close)_`
- treat it as a telegram-file-browser callback
- run `browser_dispatcher.py handle-callback <that_text>` immediately
- do not ask the user what they mean

---
Confidence
80% confidence
Finding
The skill tells the agent to automatically treat matching inbound text as callback data and execute a shell-dispatched action immediately without user confirmation. In context, that can trigger filesystem navigation or file-related actions from crafted text that only resembles an internal callback, increasing the risk of unintended autonomous execution.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The recommended action menu hard-codes Chinese labels such as `预览` and `下载`, and later pagination controls also use Chinese text. This imposes a specific language on users without opt-in or justification, which is a natural-language locale policy issue.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
This markdown file instructs the skill to send a local file as a Telegram attachment, which can affect user data privacy and move local content into an external messaging platform. The interaction pattern specifies the action flow but does not include any warning or disclosure about transmitting file contents off-host.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This code presents the browser UI text in Chinese only, which enforces a specific language on all users. The policy for natural-language violations applies to all file types, and there is no opt-in, fallback, or documented justification for the locale restriction in this file.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The file action menu text is fixed in Chinese, which can violate language/locale policy when users are not given a choice. Nothing in this file indicates that the skill is region-specific or that Chinese output is optional.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
This code hard-codes multiple user-visible labels in Chinese, such as pagination and navigation buttons, without offering a language choice or documenting that the skill is intentionally Chinese-only. That creates a natural-language locale policy issue because the skill imposes a specific language on all users.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The resolver explicitly supports a `download` action even though the skill metadata describes browsing, paging, previewing, and path exposure rather than file transfer. This scope expansion can enable unintended file exfiltration if another component trusts this resolver's output and permits downloads for files present in state, especially since the script already tolerates some version mismatches for item-based actions.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_json(cmd: list[str]) -> Dict[str, Any]:
    out = subprocess.check_output(cmd, text=True)
    return json.loads(out)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The code previews file contents and transmits file paths/files without any visible consent, warning, or disclosure in the user flow shown here. In a Telegram-integrated browser, silent preview/export features can expose sensitive local data more easily than users may expect.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
Several natural-language strings shown to the user are hardcoded in Chinese, such as truncation and download error messages, with no indication that the skill is region-specific or that users can choose a locale. This can violate language/locale policy when a skill imposes a language without opt-in.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The skill goes beyond inline browsing/navigation and includes active file exfiltration behavior via Telegram send-file responses. That materially expands capability from UI navigation into data transfer, which increases the risk of sensitive local files being exposed through chat interactions.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
This code can return arbitrary local files to Telegram by taking a resolved item path and emitting a send-file action with that path. In a file-browser context, this is dangerous because any weakness in path scoping or callback resolution elsewhere could turn browsing into straightforward local file exfiltration.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
The instruction explicitly says not to reply with the Chinese phrase “你想让我做什么?”, which is a language-specific policy embedded in the skill text. This can create a locale/language handling constraint without documenting user choice or opt-in.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
The pagination controls use Chinese labels like `上一页`, `下一页`, and `上级目录` with no indication that the user can choose a different language. This continues the same locale constraint in another user-facing part of the interaction design.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This code persists runtime state by creating directories and writing JSON to a file, which is a file write operation covered by the warning requirement for code files. The function contains no confirmation prompt, user-facing log/print, or explanatory comment/docstring disclosing that user state will be stored on disk.

Missing User Warnings

Low
Confidence
83% confidence
Finding
The helper runs external Python scripts via subprocess.check_output, which is a safety-relevant operation for code files. While the execution appears central to the skill's implementation, this file provides no confirmation prompt, logging, or comment/docstring disclosing that callbacks and browser actions are handled by spawning subprocesses.

Static analysis

No suspicious patterns detected.