T09 · Insecure Skill Coding Practices
- Location
scripts/send_message.py:270- Finding
Unrestricted image retrieval and webhook destinations enable SSRF and arbitrary file disclosure
- Content
View full analysis
Vulnerability Details
File Location:
scripts/send_message.py:40-78,scripts/send_message.py:270-283
Vulnerability Type: Server-Side Request Forgery (SSRF), arbitrary local file read, and uncontrolled outbound data transmission
Risk Level: HighVulnerable Code
python # Default webhook address if webhook_url is None: webhook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key" # Construct message data message_data = construct_message_data(content, msg_type) if message_data is None: return { "success": False, "error": f"消息格式错误: 不支持的消息类型或内容格式" } try: # Send HTTP request data = json.dumps(message_data).encode('utf-8') req = urllib.request.Request( webhook_url, data=data, headers={'Content-Type': 'application/json'}, method='POST' ) ssl_context = ssl.create_default_context() with urllib.request.urlopen( req, timeout=10, context=ssl_context ) as response: result = json.loads(response.read().decode('utf-8'))python try: # Determine whether this is a local file or URL if image_path.startswith(('http://', 'https://')): # Download image from URL with urllib.request.urlopen(image_path, timeout=10) as response: image_content = response.read() else: # Read local image file if not os.path.exists(image_path): return None with open(image_path, 'rb') as f: image_content = f.read() # Calculate MD5 md5_hash = hashlib.md5(image_content).hexdigest() # Encode as Base64 base64_content = base64.b64encode(image_content).decode('utf-8') return { "base64": base64_content, "md5": md5_hash }Technical Analysis
The
process_image()function treats any string beginning withhttp://orhttps://as a remotely retrievable image. It does not validate the hostname, resolved IP a ...[truncated 3356 chars]- Remediation
View remediation
Remediation Suggestions
-
Restrict webhook destinations
- Allowlist the expected WeCom hostname and endpoint path.
- Require HTTPS for all webhook requests.
- Reject embedded credentials, unexpected ports, fragments, and malformed URLs.
- Store approved webhook URLs in protected configuration rather than accepting arbitrary command-line values.
- Require explicit user confirmation before transmitting local or internally retrieved content to a custom destination.
-
Prevent SSRF
- Parse URLs with
urllib.parse.urlsplit()rather than relying on string prefixes. - Resolve destination hostnames and reject loopback, private, link-local, multicast, unspecified, and reserved IP ranges.
- Apply the same validation to every redirect destination, or disable redirects.
- Protect against DNS rebinding by connecting only to the validated resolved address while preserving correct TLS hostname verification.
- Allowlist trusted image-host domains when practical.
- Parse URLs with
-
Constrain local file access
- Restrict images to explicitly approved directories.
- Resolve paths with
Path.resolve()and verify that the canonical target remains beneath an approved root. - Reject symbolic links where they are not required.
- Verify that the target is a regular file.
- Validate the file using its signature rather than its extension alone.
- Reject unsupported formats before encoding or transmitting the content.
-
Enforce resource limits
- Check local file size before reading.
- Read local and remote content incrementally with a strict maximum, such as the documented 10 MB image limit.
- Reject responses with excessive
Content-Lengthvalues. - Stop streaming once the maximum accepted byte count is exceeded.
- Apply connection and total-transfer timeouts.
-
Reduce accidental disclosure
- Separate local-file, remote-image, and webhook capabilities into explicit options.
- Display the resolved source and ...[truncated 228 chars]
-
