T09 · Insecure Skill Coding Practices
- Location
scripts/capacities_client.py:59- Finding
Unrestricted API Endpoint Can Expose the Capacities Bearer Token and Private Queries
- Content
View full analysis
Vulnerability Details
File Location:
scripts/capacities_client.py, lines 59–74 and 107–124
Vulnerability Type: Unvalidated authenticated API destination
Risk Level: HighVulnerable 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
View remediation
Remediation Suggestions
- Pin the production origin to
https://api.capacities.ioand reject other destinations by default. - Parse and validate the URL before creating an authenticated request:
- Require the
httpsscheme. - Require an explicitly approved hostname.
- Reject embedded usernames or passwords.
- Reject fragments, unexpected ports, and malformed URLs.
- Require the
- Require explicit development opt-in if custom endpoints are genuinely necessary, such as a separate
CAPACITIES_ALLOW_CUSTOM_API_BASE_URL=truesetting with a clear warning. - Do not send production credentials to custom origins. Require a separate development token variable when a non-production endpoint is enabled.
- Prevent configuration-file overrides from silently replacing security-sensitive defaults. Validate
file_configthrough an allowlisted schema rather than applying unrestrictedconfig.update(file_config). - Avoid redirects across origins with credentials. Ensure redirect handling cannot forward the bearer token to an unapproved host.
- Document the endpoint override and its risks in
SKILL.md. - 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- Pin the production origin to
