T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/capacities_client.py:59
- Finding
- Unrestricted API Endpoint Can Expose the Capacities Bearer Token and Private Queries## Vulnerability Details **File Location**: `scripts/capacities_client.py`, lines 59–74 and 107–124 **Vulnerability Type**: Unvalidated authenticated API destination **Risk Level**: High ### Vulnerable Code ```python def load_config() -> dict[str, Any]: config: dict[str, Any] = { "apiBaseUrl": os.environ.get("CAPACITIES_API_BASE_URL", "https://api.capacities.io"), "timeoutMs": int(os.environ.get("CAPACITIES_TIMEOUT_MS", "15000")), "lookupCacheTtlSeconds": int(os.environ.get("CAPACITIES_LOOKUP_CACHE_TTL_SECONDS", "86400")), "verifySpacesOnSync": True, "defaultResultLimit": int(os.environ.get("CAPACITIES_DEFAULT_RESULT_LIMIT", "10")), "cacheSchemaVersion": 1, } if CONFIG_PATH.exists(): with CONFIG_PATH.open("r", encoding="utf-8") as f: file_config = json.load(f) config.update(file_config) ``` ```python def request(method: str, path: str, json_body: dict[str, Any] | None = None, retries: int = 2) -> dict[str, Any]: config = load_config() token = get_token() url = config["apiBaseUrl"].rstrip("/") + path timeout_seconds = max(int(config["timeoutMs"]) / 1000, 1) body_bytes = None headers = { "Authorization": f"Bearer {token}", "Accept": "application/json", } if json_body is not None: body_bytes = json.dumps(json_body).encode("utf-8") headers["Content-Type"] = "application/json" last_error: Exception | None = None for attempt in range(retries + 1): req = urllib.request.Request(url, data=body_bytes, headers=headers, method=method.upper()) try: with urllib.request.urlopen(req, timeout=timeout_seconds) as resp: ``` The sensitive lookup data sent through this request path is constructed at lines 164–165: ```python def lookup(space_id: str, search_term: str) -> dict[str, Any]: return request("POST", "/lookup", {"spaceId": space_id, "searchTerm": search_term}) ``` ### Technica ...[truncated 2232 chars]
- Remediation
- ## Remediation Suggestions 1. **Pin the production origin** to `https://api.capacities.io` and reject other destinations by default. 2. **Parse and validate the URL** before creating an authenticated request: - Require the `https` scheme. - Require an explicitly approved hostname. - Reject embedded usernames or passwords. - Reject fragments, unexpected ports, and malformed URLs. 3. **Require explicit development opt-in** if custom endpoints are genuinely necessary, such as a separate `CAPACITIES_ALLOW_CUSTOM_API_BASE_URL=true` setting with a clear warning. 4. **Do not send production credentials to custom origins.** Require a separate development token variable when a non-production endpoint is enabled. 5. **Prevent configuration-file overrides from silently replacing security-sensitive defaults.** Validate `file_config` through an allowlisted schema rather than applying unrestricted `config.update(file_config)`. 6. **Avoid redirects across origins with credentials.** Ensure redirect handling cannot forward the bearer token to an unapproved host. 7. **Document the endpoint override and its risks** in `SKILL.md`. 8. **Revoke and rotate the token** if there is any indication that the endpoint setting was previously redirected or plain HTTP was used. A validation pattern should enforce the trusted endpoint before constructing headers: ```python from urllib.parse import urlparse OFFICIAL_API_ORIGIN = "https://api.capacities.io" def validate_api_base_url(value: str) -> str: parsed = urlparse(value) if ( parsed.scheme != "https" or parsed.hostname != "api.capacities.io" or parsed.port not in (None, 443) or parsed.username is not None or parsed.password is not None or parsed.params or parsed.query or parsed.fragment ): raise CapacitiesConfigError( "apiBaseUrl must use the trusted Capacities HTTPS origin" ) return OFFICIAL_API_ORIGIN ```
