Back to skill

Security audit

Lark To Anything

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Feishu-to-Markdown exporter, but its converter can let document content cause unexpected network requests or writes outside the intended output folder.

Review before installing. Use this only with Feishu/Lark documents you trust, preferably in a restricted workspace and network environment. Avoid the documented global upgrade command unless you are comfortable with global npm and skill changes, and avoid exporting sensitive documents until the temporary-file, URL-validation, output-path, overwrite, and download-size issues are fixed.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/to_markdown.py:122
Finding

Insufficient URL Validation Enables Arbitrary Requests and Redirect-Based SSRF

Content
View full analysis
str | None: """Download a URL to dest (no extension) and return final path, or None.""" try: req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=15) as resp: ct = resp.headers.get('Content-Type', 'image/png') ext = _ext_from_content_type(ct) final = dest + ext with open(final, 'wb') as f: f.write(resp.read()) return final except Exception: return None ``` ```python # ── Images already rendered as ![alt](feishu-url) ── img_pattern = re.compile(r'!\[([^\]]*)\]\((https://[^)]+feishu\.cn[^)]*)\)') img_jobs: list[tuple[str, str, str]] = [] # (original_url, dest_base, alt) seen_urls: dict[str, str] = {} for i, m in enumerate(img_pattern.finditer(markdown)): url = m.group(2) if url in seen_urls: continue dest_base = os.path.join(assets_dir, f'image-{i + 1}') seen_urls[url] = dest_base img_jobs.append((url, dest_base, m.group(1))) def _dl_img(job): url, dest_base, alt = job return url, download_image(url, dest_base) ``` ### Technical Analysis The code attempts to restrict image downloads to Feishu by checking whether the complete URL contains the string `feishu.cn`. This is not equivalent to validating the parsed network hostname. For example, both of the following attacker-controlled URLs satisfy the regular expression: ```text https://attacker.example/image?source=feishu.cn https://feishu.cn.attacker.example/image ``` The URL is passed directly to `urllib.request.urlopen`. This API follows HTTP redirects by default, and the code does not validate redirect destinations. Consequently, even ...[truncated 1502 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/to_markdown.py:26
Finding

Document Title Allows Output-Directory Traversal

Content
View full analysis
str: text = text.strip() text = re.sub(r'[\\/:*?"<>|]', '_', text) text = re.sub(r'\s+', '_', text) return text or "document" ``` ```python # Derive output folder title_match = re.match(r'# (.+)', markdown) title = title_match.group(1).strip() if title_match else "document" folder_name = slugify(title) base_dir = args.output_dir or os.getcwd() doc_dir = os.path.join(base_dir, folder_name) assets_dir = os.path.join(doc_dir, 'assets') os.makedirs(doc_dir, exist_ok=True) ``` The resulting path is later used to write the Markdown file: ```python md_path = os.path.join(doc_dir, 'index.md') with open(md_path, 'w', encoding='utf-8') as f: f.write(markdown) ``` ### Technical Analysis The document title is attacker-influenced content. `slugify` replaces common path separators and reserved filename characters, but it does not reject the special path components `.` and `..`. If the title is `..`, then: ```python os.path.join(base_dir, "..") ``` refers to the parent of the requested output directory. The code creates and writes through this path without normalizing it or verifying that it remains beneath `base_dir`. The fixed output filename, `index.md`, is opened in write mode, so an existing file at the escaped location can be overwritten. ### Attack Path 1. An attacker creates or modifies a Feishu document whose converted title is `..`. 2. A user exports that document with this Skill. 3. `slugify("..")` returns `..` unchanged. 4. `doc_dir` resolves to the parent directory of `base_dir`. 5. The script creates an `assets` directory and writes `index.md` outside the user-selected output directory. 6. If an `index.md` file already exists at that location, it is truncated and replaced. ### Impact ...[truncated 421 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/to_markdown.py:122
Finding

Unbounded Concurrent Asset Downloads Can Exhaust Memory and Disk

Content
View full analysis
str | None: """Download a URL to dest (no extension) and return final path, or None.""" try: req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=15) as resp: ct = resp.headers.get('Content-Type', 'image/png') ext = _ext_from_content_type(ct) final = dest + ext with open(final, 'wb') as f: f.write(resp.read()) return final except Exception: return None ``` ```python with ThreadPoolExecutor(max_workers=6) as pool: futures = {pool.submit(_dl_img, j): j for j in img_jobs} for fut in as_completed(futures): url, local_path = fut.result() if local_path: rel = os.path.relpath(local_path, os.path.dirname(assets_dir)) # rewrite all occurrences of this URL markdown = markdown.replace(url, rel) stats['images'] += 1 else: stats['images_failed'] += 1 ``` ### Technical Analysis `resp.read()` reads the entire response body into memory before writing it to disk. The implementation does not enforce: - A maximum response size. - A maximum aggregate download size. - A maximum number of image assets. - A validated image media type. - An image-file signature check. - A meaningful read-rate or total-operation deadline. Up to six downloads run concurrently. A malicious or compromised media server can therefore return multiple very large responses and cause high simultaneous memory consumption. The responses are subsequently written to disk, permitting disk exhaustion as well. The 15-second `urlopen` timeout is not a reliable total transfer-size or total duration limit; ...[truncated 797 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:30
Finding

Unpinned Package Execution and Predictable Storage of Sensitive Document Data

Content
View full analysis
" \ --doc-format markdown \ --format json > /tmp/lark_fetch.json ``` ```bash python3 "$(dirname "$0")/scripts/to_markdown.py" /tmp/lark_fetch.json ``` ### Technical Analysis The upgrade instruction pins `@larksuite/cli`, but the package invoked through `npx skills` is not version-pinned. Depending on the local npm state and registry resolution, `npx` may download and execute a mutable package version at runtime. This creates a supply-chain boundary that is broader than required for document conversion. The fetched document is also redirected to the fixed path `/tmp/lark_fetch.json`. That file can contain sensitive document text, links, user names, media URLs, and tokens embedded in the fetched representation. A predictable shared temporary filename creates several risks: - Concurrent Skill executions can overwrite or consume each other's data. - Another local process may pre-create a symbolic link at that path. - Data may remain on disk after conversion. - File visibility depends on the process umask and platform temporary-directory configuration. The shell redirection opens the path before the Python converter runs, so the Python script cannot retroactively guarantee safe file creation. ### Attack Path #### Supply-chain path 1. The user follows the documented upgrade command. 2. `npx` resolves the unpinned `skills` package through the configured npm registry. 3. A changed, compromised, or incorrectly resolved package is downloaded. 4. Package code executes with the privileges of the user running the command. #### Temporary-file path 1. A local attacker anticipates use of `/tmp/l ...[truncated 893 chars]
Remediation
View remediation
... ``` 2. Use lockfiles, integrity verification, and a trusted registry where supported. 3. Avoid global installation unless it is strictly necessary. 4. Replace the predictable path with a securely created temporary file: ```bash tmp_file="$(mktemp)" chmod 600 "$tmp_file" trap 'rm -f "$tmp_file"' EXIT ``` 5. Quote the temporary path in every command. 6. Ensure the temporary file is owned by the current user, is not a symbolic link, and is created with restrictive permissions. 7. Remove the fetched JSON automatically after conversion, including on errors and interruptions. 8. Consider piping the JSON directly to a converter that accepts standard input, thereby avoiding persistent intermediate storage. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (5)

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding

The description frames the skill as a straightforward document export, but the workflow also triggers external tooling, downloads remote assets, and writes a local folder tree. That mismatch can mislead users and reviewers about the actual trust boundary and side effects, increasing the chance of consent bypass or unsafe execution in broader contexts.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill invokes shell commands, performs network access, reads and writes local files, but does not declare any explicit tool scope or allowed-tools boundaries. In an agent environment, missing capability declarations weakens policy enforcement and can let the skill run with broader privileges than users or reviewers expect.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
84% confidence
Finding

The upgrade instruction uses npx skills add larksuite/cli -y -g without a pinned version, which can pull whatever package version is current at execution time. That creates supply-chain and reproducibility risk, especially because this path encourages global installation of tooling that the skill later relies on.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/to_markdown.py (reported line 135)May include surrounding context.

python
def download_whiteboard(token: str, dest: str) -> str | None:
    """Download a whiteboard thumbnail via lark-cli and return final path."""
    try:
        result = subprocess.run(
            ['lark-cli', 'docs', '+media-download',
             '--type', 'whiteboard', '--token', token, '--output', dest],
            capture_output=True, text=True, timeout=30

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill does not prominently warn that execution will create local files and fetch remote assets. While this is not inherently malicious, lack of disclosure reduces informed consent and can lead to accidental writes or network activity in sensitive environments.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.