Back to skill

Security audit

TinyScraper

Security checks for vulnerabilities and agentic risk

Overview

TinyScraper is a coherent website-mirroring tool, but unsafe path handling can write or delete outside its mirror folder, so it should be reviewed before installation.

Install only if you are comfortable with a crawler that can make arbitrary HTTP requests from your agent environment and create many local files. Avoid using the cleanup command until path validation and containment checks are added, and only mirror sites you trust and are authorized to crawl.

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

T09 · Insecure Skill Coding Practices

Error
Location
lib/crawler.py:66
Finding
Arbitrary File Write Through Unsafe URL-to-Path Conversion## Vulnerability Details **File Location**: `lib/crawler.py`, lines 66-83 **Vulnerability Type**: Path traversal resulting in arbitrary file write **Risk Level**: High ### Vulnerable Code ```python def url_to_filepath(base_dir: str, url: str) -> str: parsed = urllib.parse.urlparse(url) path = parsed.path if parsed.path else "/" path = path.rstrip("/") if not path: path = "index.html" elif not os.path.splitext(path)[1]: path = path + "/index.html" path = os.path.normpath(path) if path.startswith("/"): path = path[1:] return os.path.join(base_dir, path) ``` The resulting path is subsequently passed to file-writing operations: ```python page_dir = url_to_filepath(self.base_dir, url) if not page_dir.endswith(".html"): page_dir += ".html" rewritten = rewrite_html_content(html_content, url, os.path.dirname(page_dir)) self.save_file(page_dir, rewritten.encode("utf-8")) ``` ### Technical Analysis The URL path is normalized and has only one leading slash removed. On POSIX systems, `os.path.normpath()` may preserve exactly two leading slashes. For example, a URL path such as `//tmp/payload` can remain `//tmp/payload/index.html` after normalization. Removing only the first slash produces `/tmp/payload/index.html`. Because the second argument to `os.path.join()` is now absolute, Python discards `base_dir` and returns the attacker-influenced absolute path. No canonical-path containment check is performed before `save_file()` creates parent directories and writes the downloaded content. The same unsafe conversion function is used for both pages and resources. ### Attack Path 1. An attacker controls the target website or convinces a user or Agent to mirror a crafted URL. 2. The requested URL contains a path beginning with exactly two slashes, such as `https://attacker.example//tmp/payload`. 3. The server returns attacker-controlled conte ...[truncated 700 chars]
Remediation
## Remediation Suggestions - Convert the mirror root and candidate destination to canonical absolute paths with `os.path.realpath()`. - Reject URL paths containing abnormal leading slashes, traversal components, null bytes, or platform-specific path separators. - Treat URL paths as relative components rather than removing only one leading slash. - Verify containment with `os.path.commonpath()` before creating directories or writing files. - Reject the destination if it equals or falls outside the intended mirror root. - Apply the containment check immediately before every write, not only during URL parsing. - Add regression tests for `//tmp/file`, `///tmp/file`, encoded traversal sequences, backslashes, absolute Windows paths, and symbolic-link escapes. A hardened pattern is: ```python root = os.path.realpath(base_dir) relative_path = path.lstrip("/\\") destination = os.path.realpath(os.path.join(root, relative_path)) if os.path.commonpath([root, destination]) != root: raise ValueError("Destination escapes the mirror directory") ```

T09 · Insecure Skill Coding Practices

Error
Location
lib/crawler.py:601
Finding
Arbitrary Recursive Directory Deletion Through Unsanitized Domain Input## Vulnerability Details **File Location**: `lib/crawler.py`, lines 601-608 **Vulnerability Type**: Path traversal and unrestricted recursive deletion **Risk Level**: High ### Vulnerable Code ```python if args.disconnect: domain = args.domain or input("Enter the domain to clean: ").strip() target = os.path.join(MIRRORS_DIR, domain) if os.path.exists(target): import shutil shutil.rmtree(target) log_info(f"Cleaned: {target}") else: log_warn(f"Directory does not exist: {target}") ``` ### Technical Analysis The cleanup argument is used directly as a filesystem path component. The code does not verify that the supplied value is a valid hostname and does not confirm that the canonical destination remains beneath `MIRRORS_DIR`. If `domain` is an absolute path, `os.path.join(MIRRORS_DIR, domain)` discards `MIRRORS_DIR`. A value containing `../` components can likewise traverse to a parent directory. The resulting path is passed to `shutil.rmtree()`, which recursively deletes the selected directory. Merely checking `os.path.exists()` provides no security boundary and does not prevent deletion outside the mirror directory. ### Attack Path 1. An attacker, malicious automation, or untrusted instruction controls the cleanup domain argument. 2. The attacker supplies an absolute path or traversal value, such as `/home/user/project` or `../../project`. 3. `os.path.join()` constructs a path outside `MIRRORS_DIR`. 4. The existence check succeeds for the selected directory. 5. `shutil.rmtree()` recursively removes the directory and its contents. ### Impact Assessment This vulnerability permits recursive deletion of any directory writable by the crawler process. It can destroy the OpenClaw workspace, user files, application source, configuration, or other crawler outputs. The vulnerability does not independently elevate operating-system privileges, but its deletion scope ...[truncated 111 chars]
Remediation
## Remediation Suggestions - Validate the input as a hostname rather than accepting arbitrary path syntax. - Reject absolute paths, path separators, empty values, dot components, and traversal sequences. - Canonicalize both `MIRRORS_DIR` and the proposed target using `os.path.realpath()`. - Require `os.path.commonpath([mirror_root, target]) == mirror_root`. - Explicitly reject deletion of the mirror root itself. - Consider maintaining a registry of mirror directories created by the crawler and permit cleanup only for registered entries. - Refuse to follow or delete through symbolic links. - Add an explicit confirmation step when cleanup is initiated interactively. - Add tests covering absolute paths, `../` traversal, nested traversal, symbolic links, path separators, and deletion of the mirror root. A hardened containment check should resemble: ```python root = os.path.realpath(MIRRORS_DIR) target = os.path.realpath(os.path.join(root, validated_domain)) if target == root or os.path.commonpath([root, target]) != root: raise ValueError("Refusing to delete outside the mirror directory") ```

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
lib/crawler.py:390
Finding
Server-Side Request Forgery Through Unrestricted URL Fetching## Vulnerability Details **File Location**: `lib/crawler.py`, lines 390-410 **Vulnerability Type**: Server-side request forgery and internal network access **Risk Level**: Medium ### Vulnerable Code ```python def download_url(self, url: str, timeout: int = TIMEOUT) -> tuple[Optional[bytes], Optional[str]]: try: req = urllib.request.Request( url, headers={"User-Agent": USER_AGENT} ) with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read() content_type = resp.headers.get("Content-Type", "") if ";" in content_type: content_type = content_type.split(";")[0].strip() return body, content_type except Exception as e: log_warn(f"Download failed: {url} -> {e}") self.failed.append(url) self.stats["failed"] += 1 return None, None ``` ### Technical Analysis User-selected URLs are passed to `urllib.request.urlopen()` without validating the destination network address. The code does not block loopback, private, link-local, reserved, or cloud metadata addresses. `urllib` follows HTTP redirects by default, but the code does not validate each redirect destination. Therefore, even a URL initially pointing to a public host may redirect to an internal service. Hostname-only same-domain checks elsewhere in the crawler do not protect the initial request and do not validate the final address after DNS resolution or redirection. The crawler reads the complete response and stores it locally. This permits requests to services reachable from the Agent host but not necessarily reachable by the party supplying the URL. ### Attack Path 1. An attacker supplies an internal URL, a hostname resolving to an internal address, or a public URL that redirects to an internal endpoint. 2. The Agent invokes TinyScraper with the supplied URL. 3. `urllib.request.ur ...[truncated 846 chars]
Remediation
## Remediation Suggestions - Restrict input to explicitly permitted `http` and `https` URLs. - Resolve the hostname before connecting and reject loopback, private, link-local, multicast, reserved, unspecified, and other non-public addresses for both IPv4 and IPv6. - Disable automatic redirects or validate every redirect target before following it. - Re-resolve and validate the destination immediately before connection to reduce DNS-rebinding exposure. - Reject URLs containing credentials or ambiguous hostname syntax. - Consider an explicit hostname allowlist when the crawler operates in a sensitive environment. - Route outbound requests through an egress proxy that blocks private and metadata address ranges. - Apply response-size and request-count limits to reduce denial-of-service exposure. - Add tests for loopback addresses, private IPv4 ranges, IPv6 loopback and unique-local addresses, alternative IP representations, DNS rebinding, and public-to-private redirects.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (11)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill performs network access and local file operations but does not declare an explicit tool scope or permissions boundary. That makes it easier for an agent or user to invoke a capability-rich skill without clear consent and review, especially since it writes mirrored content locally and includes a delete function.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The activation phrases are broad enough to match ordinary requests like saving a webpage, which can cause the scraper to be invoked when the user did not intend a full-site crawl. In this skill, accidental activation is more dangerous because the behavior triggers network enumeration and bulk local writes across an entire domain.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill describes downloading an entire site to `tmp/mirrors/{domain}/` and supports deleting existing mirrors, but it does not clearly warn users about local writes, disk usage, or destructive deletion behavior. This can lead to unintended data creation or removal on the host, especially when users think they are only previewing or saving a small amount of content.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The module docstring and user-facing descriptions are written exclusively in Chinese, and the CLI/help/logging strings throughout the file also assume Chinese as the interaction language. This creates a natural-language locale constraint without offering the user a language choice or documenting a justified region-specific limitation.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The skill’s stated purpose is site mirroring/downloading, but the CLI also exposes a deletion feature that recursively removes local mirror data. Even if intended as cleanup, this expands the tool’s capabilities beyond its declared scope and can delete arbitrary directories under the mirrors root when given an untrusted or malformed domain value.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The code builds a filesystem path from user-controlled 'domain' input and passes it directly to shutil.rmtree without canonicalization or containment checks. An attacker or mistaken user could supply path traversal values such as '../../...' to recursively delete files outside the intended mirror directory, causing destructive local data loss.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The spec exposes a destructive cleanup command (`tinyscraper -d example.com`) but provides no warning, confirmation flow, or scope validation guidance. In a tool that writes under a workspace directory, unclear deletion semantics can lead to accidental data loss or over-broad directory removal if implementers infer unsafe behavior.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
文件标题、描述与操作说明均以中文给出,未说明这是面向特定中文用户群或地区的限定技能,也未提供语言可选项。按语言/区域策略,若技能隐含强制特定语言,应当提供用户选择或给出明确合理的约束说明。

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
All natural-language instructions, headings, and operational guidance in the file are Chinese-only. This can violate a language/locale policy when no user opt-in or documented region-specific justification is provided.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
This code file contains user-facing natural-language strings in Chinese in the module docstring and later console output, but provides no indication that the language is configurable or optional. Under the policy, forcing a specific language without user opt-in is a natural-language policy concern.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The script prints headings and summary messages only in Chinese, which imposes a specific language on users without offering a choice. This matches the policy's language/locale violation pattern for natural-language content in code string literals.

Static analysis

No suspicious patterns detected.