T09 · Insecure Skill Coding Practices
- Location
scripts/gitlab_client.py:38- Finding
GitLab Token Can Be Exfiltrated to an Attacker-Controlled Host
- Content
View full analysis
tuple[str, str, str]: """ Parse a GitLab MR URL into (host, project_path_encoded, mr_iid). Supports: https://gitlab.com/group/subgroup/project/-/merge_requests/123 https://gitlab.example.com/namespace/project/-/merge_requests/456 Returns (host, url_encoded_project_path, mr_iid). """ pattern = r"(https?://[^/]+)/(.+)/-/merge_requests/(\d+)" m = re.match(pattern, url.rstrip("/")) if not m: raise ValueError(f"Cannot parse MR URL: {url}") host = m.group(1) project_path = m.group(2) mr_iid = m.group(3) encoded_path = project_path.replace("/", "%2F") return host, encoded_path, mr_iid def api_get(host: str, token: str, path: str, params: dict | None = None) -> dict | list: url = f"{host}/api/v4{path}" if params: url += "?" + urlencode(params) req = Request(url, headers={"PRIVATE-TOKEN": token, "Content-Type": "application/json"}) try: with urlopen(req) as resp: return json.loads(resp.read().decode()) except HTTPError as e: body = e.read().decode() raise RuntimeError(f"GitLab API error {e.code} on GET {path}: {body}") from e ``` The vulnerable data flow is invoked as follows: ```python def fetch_mr(mr_url: str) -> dict: """Return MR metadata (title, author, source_branch, target_branch, description, state).""" creds = load_credentials() host, project, iid = parse_mr_url(mr_url) data = api_get(host, creds["token"], f"/projects/{project}/merge_requests/{iid}") ``` ### Technical Analysis The Skill loads a privileged GitLab token from `~/.openclaw/credentials/gitlab.json`, but it does not bind that token to the configured GitLab ho ...[truncated 1865 chars]- Remediation
View remediation
None: supplied = urlsplit(mr_url) configured = urlsplit(configured_host) if supplied.scheme != "https": raise ValueError("MR URLs must use HTTPS") if ( supplied.hostname != configured.hostname or supplied.port != configured.port or supplied.username is not None or supplied.password is not None ): raise ValueError("MR URL origin does not match the configured GitLab host") def fetch_mr(mr_url: str) -> dict: creds = load_credentials() validate_mr_origin(mr_url, creds["host"]) _, project, iid = parse_mr_url(mr_url) return api_get( creds["host"].rstrip("/"), creds["token"], f"/projects/{project}/merge_requests/{iid}", ) ``` ]]>
