T09 · Insecure Skill Coding Practices
- Location
run_all.py:683- Finding
Unrestricted User-Controlled URL Fetch Enables SSRF
- Content
View full analysis
str: r = subprocess.run( ['curl', '-s', '-L', f'--max-time', str(timeout), '-H', 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', '-H', 'Accept: text/html,application/json', '-H', 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8', url], capture_output=True, text=True ) return r.stdout if r.returncode == 0 else '' ``` ```python def fetch_part7(rwa_url: str = '') -> tuple: log('Part.7 - searching PANews RWA Weekly...') if not rwa_url: rwa_url = find_rwa_article() if not rwa_url: log('[WARN] Part.7 - could not find article URL') return [], '' log(f'Part.7 - fetching: {rwa_url}') html = fetch(rwa_url) ``` ```python parser.add_argument( '--rwa-url', help='PANews RWA Weekly article URL (skip automatic search)', default='' ) ... rwa_projects, rwa_url = fetch_part7(args.rwa_url) ``` ### Technical Analysis The `--rwa-url` command-line value is passed to `curl` without validating its scheme, hostname, port, resolved address, or redirect destination. The documentation describes this value as a PANews article URL, but the implementation does not enforce that restriction. An attacker who can influence the command-line arguments can therefore cause the Skill to issue requests to arbitrary destinations, including: - Services bound to localhost - Private network addresses - Link-local or cloud metadata endpoints - Services on nonstandard ports - Local resources supported by the installed curl build - External hosts reached through redirects The use of `curl -L` expands the issue because even a URL initially hosted on an approved domain could redirect to ...[truncated 1278 chars]- Remediation
View remediation
`. 9. Where possible, accept only an article identifier and construct the complete trusted URL internally rather than accepting a complete URL. Example validation policy: ```python from urllib.parse import urlsplit import ipaddress import socket ALLOWED_HOSTS = {'www.panewslab.com'} def validate_rwa_url(url): parsed = urlsplit(url) if parsed.scheme != 'https': raise ValueError('Only HTTPS URLs are permitted') if parsed.hostname not in ALLOWED_HOSTS: raise ValueError('Only PANews URLs are permitted') if parsed.username or parsed.password or parsed.port not in (None, 443): raise ValueError('Credentials and custom ports are prohibited') if not parsed.path.startswith('/en/articles/'): raise ValueError('Unexpected PANews article path') for info in socket.getaddrinfo(parsed.hostname, 443): address = ipaddress.ip_address(info[4][0]) if not address.is_global: raise ValueError('Non-public destination prohibited') return url ``` ]]>
