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]
