T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/linz_transport.py:23
- Finding
- Unrestricted API Base URL Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/linz_transport.py:23-39, 43-48, 286-293` **Vulnerability Type**: Server-Side Request Forgery through an unrestricted network destination **Risk Level**: Medium ### Complete Code Snippet ```python def build_url(base_url: str, path: str) -> str: base = base_url.rstrip("/") return f"{base}/{path.lstrip('/')}" def http_get_json(url: str, timeout: int) -> Any: req = urllib.request.Request(url=url, method="GET") try: with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read().decode("utf-8") return json.loads(body) except urllib.error.HTTPError as err: raise RuntimeError(f"HTTP {err.code} for {url}") from err except urllib.error.URLError as err: raise RuntimeError(f"Network error for {url}: {err.reason}") from err except json.JSONDecodeError as err: raise RuntimeError(f"Invalid JSON from {url}: {err}") from err def query_efa(base_url: str, endpoint: str, params: dict[str, Any], timeout: int) -> dict[str, Any]: query = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None}) url = build_url(base_url, f"/efa/{endpoint}?{query}") payload = http_get_json(url, timeout) if not isinstance(payload, dict): raise RuntimeError(f"Expected JSON object from {endpoint}") return payload ``` ```python parser.add_argument( "--base-url", default=os.environ.get("LINZ_TRANSPORT_API_BASE_URL", DEFAULT_BASE_URL), help=( "API base URL. Defaults to LINZ_TRANSPORT_API_BASE_URL or " f"{DEFAULT_BASE_URL}." ), ) ``` The corresponding behavior is explicitly documented in `SKILL.md:26-29, 35-38`, which permits the base URL to be supplied through either a command-line argument or an environment variable. ### Technical Analysis The script accepts a caller-controlled base URL and passes the resulting URL directly to `urllib.request.urlopen`. It does not ...[truncated 2338 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Default to and allowlist the official origin: - Scheme: `https` - Host: `www.linzag.at` - Expected path prefix: `/linz2` - Expected port: `443` 2. If custom endpoints are operationally necessary, require an explicit trusted-development option rather than accepting arbitrary destinations during normal execution. 3. Parse custom URLs with `urllib.parse.urlsplit` and reject: - Schemes other than HTTPS - URLs containing usernames or passwords - Unexpected ports - Empty or malformed hostnames - Loopback, private, link-local, multicast, unspecified, and reserved IP addresses 4. Resolve the hostname and validate every returned address before connecting. Revalidate the actual connection destination to reduce DNS-rebinding risk. 5. Disable automatic redirects or validate every redirect target using the same origin and IP-address policy. 6. Apply an execution-level network policy that permits outbound access only to the official transit API host. Application validation should not be the sole SSRF control. 7. Document the security boundary clearly and remove general user-facing encouragement to supply arbitrary base URLs. ]]>
