T09 · Insecure Skill Coding Practices
- Location
scripts/open_page.py:95- Finding
Unrestricted URL Navigation Through Privileged Browser Profiles
- Content
View full analysis
None: subprocess.run([cmd, url], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) ``` ```python url = req.get("url") if not url or not isinstance(url, str): print("Error: url is required", file=sys.stderr) return 1 mode = str(req.get("mode", "auto")).lower() timeout = int(req.get("timeout", 20)) cmd, chosen_mode = choose(mode) port = resolve_port(mode, url) open_url(cmd, url) ``` ### Technical Analysis The input validation only verifies that `url` is a non-empty string. It does not enforce an `http` or `https` scheme, reject local browser schemes, validate the hostname, or restrict access to loopback, link-local, and private-network destinations. The unvalidated value is passed as an argument to one of the fixed external browser wrappers: - `/home/jack/.local/bin/oc-cn` - `/home/jack/.local/bin/oc-global` - `/home/jack/.local/bin/oc-browser` The use of an argument list rather than `shell=True` prevents conventional shell metacharacter injection. However, it does not prevent the browser or wrapper from interpreting the supplied value as an unsupported scheme, an internal-network destination, or potentially a command-line option. The behavior of the external wrappers could not be verified because their implementations are not included in the audited project. The risk is increased because the Skill intentionally uses persistent “real browser” profiles, which may contain authenticated sessions unavailable to an ordinary unauthenticated HTTP client. ### Attack Path 1. An attacker or untrusted caller supplies a crafted request containing a loopback, private-network, link-local, or non-web URL. 2. `read_request()` parses the input without applying URL security policy. ...[truncated 1010 chars]- Remediation
View remediation
str: parsed = urllib.parse.urlsplit(value) if parsed.scheme.lower() not in {"https", "http"}: raise ValueError("only HTTP and HTTPS URLs are allowed") if not parsed.hostname: raise ValueError("URL must contain a hostname") if parsed.username is not None or parsed.password is not None: raise ValueError("embedded credentials are not allowed") for result in socket.getaddrinfo(parsed.hostname, parsed.port): address = ipaddress.ip_address(result[4][0]) if ( address.is_private or address.is_loopback or address.is_link_local or address.is_multicast or address.is_reserved or address.is_unspecified ): raise ValueError("internal or reserved destinations are not allowed") return value ``` ]]>
