Back to skill

Security audit

U2-doc-parser

Security checks for vulnerabilities and agentic risk

Overview

The skill clearly discloses that it uploads documents to an external UniDoc service, but its implementation has under-scoped network fetching and a force option that can delete directories.

Review before installing. Use only non-sensitive test documents, since files are uploaded to an external unauthenticated UAT service. Avoid --skip-security-check and do not set UNIDOC_BASE_URL or UNIDOC_API_KEY unless you fully trust the endpoint. Do not use --force with paths that may be directories until the recursive deletion behavior is fixed.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/unidoc_parse.py:343
Finding
Unrestricted Server-Controlled URL Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/unidoc_parse.py:343-352` **Vulnerability Type**: Server-Side Request Forgery through an insufficiently validated API response **Risk Level**: High ### Vulnerable Code ```python file_url = export_res.get("result") if not file_url: raise ValueError(f"Export failed: {export_res.get('message', 'Unknown error')}") # 验证返回的 URL 是否安全 if not isinstance(file_url, str) or not file_url.startswith(('http://', 'https://')): raise ValueError(f"Invalid file URL returned: {file_url}") content = requests.get(file_url, timeout=60).content.decode('utf-8') return content ``` ### Technical Analysis The export API controls `file_url`, which is passed directly to `requests.get()`. Validation only confirms that the value begins with `http://` or `https://`. It does not: - Restrict requests to an approved UniDoc hostname. - Require encrypted HTTPS connections. - Reject loopback, private, link-local, or reserved IP addresses. - Reject URLs containing embedded credentials. - Validate DNS resolution results. - Disable redirects or validate redirect destinations. - Limit the downloaded response size. Consequently, a compromised, malicious, spoofed, or user-configured UniDoc API endpoint can direct the client to an arbitrary network resource. The request originates from the system running the Skill and therefore may reach services inaccessible to the remote attacker. The downloaded response is decoded and returned as converted document content. The caller subsequently prints that content to standard output or writes it to the selected output file, creating a channel through which responses from internal services may be disclosed. ### Attack Path 1. An attacker compromises or impersonates the configured UniDoc endpoint, or persuades the user to set `UNIDOC_BASE_URL` to an attacker-controlled service. 2. The user invokes the Skill with a document. 3. The attacker-controlled `/exportFile` response supplies a URL ...[truncated 1331 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require `https` for every export URL. 2. Maintain an explicit allowlist of trusted export hostnames rather than accepting arbitrary hosts. 3. Parse URLs with `urllib.parse.urlparse()` and reject: - Embedded usernames or passwords. - Unexpected ports. - Missing or malformed hostnames. 4. Resolve the hostname and reject every address in loopback, private, link-local, multicast, reserved, and unspecified ranges. 5. Disable automatic redirects, or validate the destination of every redirect using the same rules. 6. Pin export downloads to the expected UniDoc domain when the API contract permits it. 7. Apply a maximum response-size limit and validate the expected content type before processing the body. 8. Do not permit plaintext HTTP through `--skip-security-check`; fail closed when the endpoint or export URL is not HTTPS. 9. Consider using an opaque file identifier with a fixed trusted download endpoint instead of accepting an arbitrary URL from the API. Example design: ```python from urllib.parse import urlparse import ipaddress import socket ALLOWED_EXPORT_HOSTS = {"unidoc.uat.hivoice.cn"} def validate_export_url(value: str) -> str: parsed = urlparse(value) if parsed.scheme != "https": raise ValueError("Export URL must use HTTPS") if parsed.username or parsed.password: raise ValueError("Credentials are not allowed in export URLs") if parsed.hostname not in ALLOWED_EXPORT_HOSTS: raise ValueError("Untrusted export hostname") for result in socket.getaddrinfo(parsed.hostname, parsed.port or 443): address = ipaddress.ip_address(result[4][0]) if ( address.is_private or address.is_loopback or address.is_link_local or address.is_reserved or address.is_multicast or address.is_unspecified ): raise ValueError("Export URL resolves to a prohibited address") return value ` ...[truncated 106 chars]

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/unidoc_parse.py:138
Finding
Force Option Can Recursively Delete an Arbitrary Existing Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/unidoc_parse.py:138-153` **Vulnerability Type**: Unsafe recursive deletion through an output-file option **Risk Level**: Medium ### Vulnerable Code ```python else: # 是目录或其他类型 path_type = "directory" if os.path.isdir(safe_path) else "non-file path" if force: print(f"[WARN] Removing existing {path_type}: {safe_path}", file=sys.stderr) try: if os.path.isdir(safe_path): import shutil shutil.rmtree(safe_path) else: os.remove(safe_path) except OSError as e: raise PermissionError(f"Cannot remove existing {path_type}: {output_path}") from e else: raise ValueError( f"Path exists but is a {path_type}: {output_path}\n" f" Use --force to overwrite" ) ``` ### Technical Analysis The `--output` argument is intended to identify a single output file. However, when the supplied path resolves to an existing directory and `--force` is enabled, validation recursively deletes the entire directory with `shutil.rmtree()`. Recursive directory deletion is not necessary for document conversion or file replacement. The preceding system-directory denylist only protects a limited set of paths and does not protect user home directories, source repositories, mounted data, application directories, or other valuable locations writable by the current process. Because `sanitize_path()` resolves the path to its real absolute location, any accepted writable directory outside the limited denylist can become the recursive deletion target. ### Attack Path 1. A user or automation invokes the parser with `--force`. 2. The `--output` argument points to an existing valuable directory, whether accidentally or through attacker-influenced command arguments. 3. `validate_output_path()` identifies the destination as a directory. 4. Because `force` is true, the function calls `sh ...[truncated 1050 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Never delete a directory when processing an output-file argument. 2. Reject all existing non-regular-file destinations, regardless of whether `--force` is enabled. 3. Limit `--force` to replacing an existing regular file. 4. Write converted output to a temporary file in the destination directory and atomically replace the intended regular file with `os.replace()`. 5. Reject symbolic-link output destinations if the security model requires preventing writes outside the selected directory. 6. Consider requiring output paths to remain within an explicitly approved workspace. 7. Add tests confirming that `--force` cannot delete directories, special files, or unrelated paths. A safer validation branch would be: ```python if os.path.exists(safe_path): if not os.path.isfile(safe_path): raise ValueError( f"Output path must be a regular file: {output_path}" ) if not force: raise FileExistsError( f"Output file already exists: {output_path}" ) if not os.access(safe_path, os.W_OK): raise PermissionError( f"Output file is not writable: {output_path}" ) ``` The existing `shutil.rmtree()` behavior should be removed entirely. ]]>
Vulnerability Patterns
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (12)

Tainted flow: 'SYNC_UPLOAD_URL' from os.getenv (line 31, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
with open(file_path, 'rb') as file:
            files = {'file': file}
            response = requests.post(
                SYNC_UPLOAD_URL,
                data=body,
                files=files,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'ASYNC_UPLOAD_URL' from os.getenv (line 32, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
with open(file_path, 'rb') as file:
            files = {'file': file}
            response = requests.post(
                ASYNC_UPLOAD_URL,
                data=body,
                files=files,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'STATUS_URL' from os.getenv (line 34, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
raise TimeoutError("File conversion timed out after 5 minutes")

            params = {"fileId": file_id}
            response = requests.get(
                url=STATUS_URL,
                params=params,
                headers=headers,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'EXPORT_URL' from os.getenv (line 33, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
headers['Authorization'] = f'Bearer {API_KEY}'

        params = {"fileId": file_id, "targetType": self.target_type}
        response = requests.get(
            url=EXPORT_URL,
            params=params,
            headers=headers,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'file_url' from requests.get (line 343, network input) → requests.get (network output)

Medium
Category
Data Flow
Content
if not isinstance(file_url, str) or not file_url.startswith(('http://', 'https://')):
            raise ValueError(f"Invalid file URL returned: {file_url}")

        content = requests.get(file_url, timeout=60).content.decode('utf-8')
        return content
Confidence
98% confidence
Finding
The code fetches a second URL returned by the remote API and only checks that it starts with http:// or https://. A malicious or compromised service can supply an arbitrary URL, turning this into SSRF that can make the host contact internal services or attacker infrastructure, and the use of plain HTTP also permits man-in-the-middle redirection or content tampering.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
func: str = "unisound",
    uid: Optional[str] = None,
    output_path: Optional[str] = None,
    skip_security_check: bool = False,
    force: bool = False
) -> str:
    """
Confidence
80% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
func: str = "unisound",
    uid: Optional[str] = None,
    output_path: Optional[str] = None,
    skip_security_check: bool = False,
    force: bool = False
) -> str:
    """
Confidence
80% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
func: str = "unisound",
    uid: Optional[str] = None,
    output_path: Optional[str] = None,
    skip_security_check: bool = False,
    force: bool = False
) -> str:
    """
Confidence
80% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
func: str = "unisound",
    uid: Optional[str] = None,
    output_path: Optional[str] = None,
    skip_security_check: bool = False,
    force: bool = False
) -> str:
    """
Confidence
80% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
:return: 转换后的内容
    """
    # 安全检查
    check_environment_security(skip_interactive=skip_security_check)

    # 验证输入文件
    validate_input_file(file_path)
Confidence
80% confidence
Finding
Passing skip_security_check into check_environment_security allows callers to suppress the interactive safeguard that otherwise blocks plaintext HTTP use. In this tool's context, which uploads local documents to an external service and already warns that UAT lacks authentication, making it easy to bypass transport-security friction increases the risk of accidental document disclosure over insecure channels.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
)

    parser.add_argument(
        "--skip-security-check",
        action="store_true",
        help="Skip security warnings (not recommended)"
    )
Confidence
80% confidence
Finding
The CLI exposes --skip-security-check, which lets users bypass safeguards before uploading files to a third-party endpoint. In a document-processing skill, this is more dangerous because the primary asset is user-provided document content, and bypassing transport or environment warnings can directly facilitate exfiltration of sensitive data to insecure or unauthenticated services.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The natural-language comments and metadata in the header are written entirely in Chinese, which reflects a fixed language choice in the skill content. There is no accompanying indication that the skill is region-specific or that users can opt into this locale, which can conflict with organizational language/locale policy requirements.

Static analysis

No suspicious patterns detected.