T09 · Insecure Skill Coding Practices
- Location
scripts/save_wechat_mp_to_notion.py:27- Finding
Unrestricted Source URL Enables Server-Side Request Forgery and Content Exfiltration
- Content
View full analysis
str: req = urllib.request.Request(url, headers={"User-Agent": UA}) with urllib.request.urlopen(req, timeout=30) as resp: return resp.read().decode("utf-8", "ignore") ``` The unrestricted argument is passed to `fetch()`, and extracted content is subsequently uploaded to Notion: ```python ap.add_argument("wechat_url") ap.add_argument("notion_parent_id") ap.add_argument("--parent-type", choices=["page", "database"], default="page") args = ap.parse_args() html_data = fetch(args.wechat_url) title, author, publish_date = extract_metadata(html_data) content_items = parse_article(html_data) page = create_page( args.notion_parent_id, args.parent_type, title, author, publish_date, args.wechat_url ) append_blocks(page["id"], content_items) ``` ### Technical Analysis The Skill documentation declares that the source should be a public WeChat article at `https://mp.weixin.qq.com/s/...`. However, the script does not enforce that restriction. It accepts any string as `wechat_url` and passes it directly to `urllib.request.urlopen()`. The implementation does not validate: - The URL scheme. - The normalized hostname. - The expected `/s/` path. - Embedded credentials or nonstandard ports. - DNS resolution to loopback, private, link-local, or reserved addresses. - The destinations of HTTP redirects. Consequently, a caller may direct the runtime to services that are not part of the declared functionality, including localhost endpoints, private-network services, cloud metadata endpoints, or other URLs accessible from the host. After fetching the response, the script parses it and sends extracted metadata and article blocks to the fixed Notion API. Responses containing a recognized `js_co ...[truncated 2577 chars]- Remediation
View remediation
str: parsed = urlsplit(url) if parsed.scheme != "https": raise ValueError("Only HTTPS WeChat article URLs are allowed") if parsed.username or parsed.password: raise ValueError("URL credentials are not allowed") if (parsed.hostname or "").lower() != "mp.weixin.qq.com": raise ValueError("Only mp.weixin.qq.com is allowed") if parsed.port not in (None, 443): raise ValueError("Nonstandard ports are not allowed") if not parsed.path.startswith("/s/"): raise ValueError("URL is not a supported WeChat article path") for result in socket.getaddrinfo(parsed.hostname, 443): address = ipaddress.ip_address(result[4] ...[truncated 560 chars]
